Dalam SQL Server, haruskah saya memaksa LOOP BERGABUNG dalam kasus berikut?

15

Biasanya, saya sarankan agar tidak menggunakan petunjuk bergabung untuk semua alasan standar. Baru-baru ini, bagaimanapun, saya telah menemukan pola di mana saya hampir selalu menemukan loop paksa bergabung untuk berkinerja lebih baik. Bahkan, saya mulai menggunakan dan sangat merekomendasikannya sehingga saya ingin mendapatkan pendapat kedua untuk memastikan saya tidak melewatkan sesuatu. Berikut adalah skenario yang representatif (kode yang sangat spesifik untuk menghasilkan contoh ada di akhir):

--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID

--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID

SampleTable memiliki 1 juta baris dan PK-nya adalah ID.
Tabel temp #Driver hanya memiliki satu kolom, ID, tidak ada indeks, dan baris 50 ribu.

Yang saya temukan secara konsisten adalah sebagai berikut:

Kasus 1: TIDAK ADA
Pindaian Indeks PETUNJUK pada SampleTable
Hash Bergabung dengan
Durasi yang Lebih Tinggi (rata-rata 333 ms)
CPU yang lebih tinggi (rata-rata 331 ms) Pembacaan
Logika Rendah (4714)

Kasus 2: LOOP JOIN HINT
Index Cari pada SampleTable
Loop Bergabung dengan
Durasi Lebih Rendah (rata-rata 204ms, 39% lebih sedikit)
CPU lebih rendah (rata-rata 206, 38% lebih sedikit)
Bacaan Logika Jauh Lebih Tinggi (160015, 34X lebih)

Pada awalnya, bacaan yang jauh lebih tinggi dari kasus kedua membuatku sedikit takut karena menurunkan bacaan sering dianggap sebagai ukuran kinerja yang layak. Tetapi semakin saya berpikir tentang apa yang sebenarnya terjadi, itu tidak menjadi perhatian saya. Inilah pemikiran saya:

SampleTable terkandung pada 4714 halaman, membutuhkan waktu sekitar 36MB. Kasus 1 memindai semuanya, itulah sebabnya kami mendapatkan 4714 pembacaan. Selanjutnya, ia harus melakukan 1 juta hash, yang merupakan CPU intensif, dan yang akhirnya mendorong waktu secara proporsional. Ini semua hashing yang tampaknya menaikkan waktu dalam kasus 1.

Sekarang pertimbangkan kasus 2. Ini tidak melakukan hashing apa pun, tetapi malah melakukan 50000 pencarian terpisah, yang adalah apa yang mendorong pembacaan. Tetapi seberapa mahalkah pembacaannya secara komparatif? Orang mungkin mengatakan bahwa jika itu adalah bacaan fisik, itu bisa sangat mahal. Tetapi perlu diingat 1) hanya pembacaan pertama dari halaman yang diberikan dapat fisik, dan 2) meskipun demikian, kasus 1 akan memiliki masalah yang sama atau lebih buruk karena dijamin akan mengenai setiap halaman.

Jadi memperhitungkan fakta bahwa kedua kasus harus mengakses setiap halaman setidaknya sekali, tampaknya menjadi pertanyaan mana yang lebih cepat, 1 juta hash atau sekitar 155.000 dibaca terhadap memori? Tes saya tampaknya mengatakan yang terakhir, tetapi SQL Server secara konsisten memilih yang pertama.

Pertanyaan

Jadi kembali ke pertanyaan saya: Haruskah saya terus memaksakan petunjuk LOOP JOIN ini saat pengujian menunjukkan hasil seperti ini, atau apakah saya melewatkan sesuatu dalam analisis saya? Saya ragu-ragu untuk menentang pengoptimal SQL Server, tapi rasanya seperti beralih menggunakan hash bergabung jauh lebih awal dari yang seharusnya dalam kasus-kasus seperti ini.

Pembaruan 2014-04-28

Saya melakukan beberapa pengujian lagi, dan menemukan bahwa hasil yang saya peroleh di atas (pada VM w / 2 CPU) saya tidak dapat meniru di lingkungan lain (saya mencoba 2 mesin fisik yang berbeda dengan 8 & 12 CPU). Pengoptimal melakukan jauh lebih baik dalam kasus-kasus terakhir ke titik di mana tidak ada masalah yang diucapkan seperti itu. Saya kira pelajaran yang dipetik, yang tampaknya jelas dalam retrospeksi, adalah bahwa lingkungan dapat secara signifikan mempengaruhi seberapa baik pengoptimal bekerja.

Rencana Eksekusi

Rencana Eksekusi Kasus 1 Paket 1 Rencana Eksekusi Kasus 2 masukkan deskripsi gambar di sini

Kode Untuk Menghasilkan Contoh Kasus

------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------    

CREATE TABLE SampleTable
    (  
       ID         INT NOT NULL PRIMARY KEY CLUSTERED
     , Number1    INT NOT NULL
     , Number2    INT NOT NULL
     , Number3    INT NOT NULL
     , Number4    INT NOT NULL
     , Number5    INT NOT NULL
    )

--Add 1 million rows
;WITH  
    Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows  
    Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows  
    Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows 
    Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows 
    Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows 
    Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows 
    FinalCte AS (SELECT  ROW_NUMBER() OVER (ORDER BY C) AS Number FROM   Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM  FinalCte
WHERE Number <= 1000000

------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------    
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
    SELECT S.*
    INTO #Results
    FROM #Driver AS D
    INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO

------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------    
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0

------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------    

GO
/*Reg*/  EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint


------------------------------------------------------------
-- Results
------------------------------------------------------------    

/*

Duration CPU   Reads    TextData
315      313   4714     /*Reg*/  EXEC JoinTest_NoHint
309      296   4713     /*Reg*/  EXEC JoinTest_NoHint
327      329   4713     /*Reg*/  EXEC JoinTest_NoHint
398      406   4715     /*Reg*/  EXEC JoinTest_NoHint
316      312   4714     /*Reg*/  EXEC JoinTest_NoHint
217      219   160017   /*Loop*/ EXEC JoinTest_LoopHint
211      219   160014   /*Loop*/ EXEC JoinTest_LoopHint
217      219   160013   /*Loop*/ EXEC JoinTest_LoopHint
190      188   160013   /*Loop*/ EXEC JoinTest_LoopHint
187      187   160015   /*Loop*/ EXEC JoinTest_LoopHint

*/
JohnnyM
sumber

Jawaban:

13

SampleTable terkandung pada 4714 halaman, membutuhkan waktu sekitar 36MB. Kasus 1 memindai semuanya, itulah sebabnya kami mendapatkan 4714 pembacaan. Selanjutnya, ia harus melakukan 1 juta hash, yang merupakan CPU intensif, dan yang akhirnya mendorong waktu secara proporsional. Ini semua hashing yang tampaknya menaikkan waktu dalam kasus 1.

Ada biaya awal untuk bergabung dengan hash (membangun tabel hash, yang juga merupakan operasi pemblokiran), tetapi hash bergabung pada akhirnya memiliki biaya per-baris teoritis terendah dari tiga jenis gabungan fisik yang didukung oleh SQL Server, keduanya di ketentuan IO dan CPU. Hash join benar-benar hadir dengan input build yang relatif kecil dan input probe yang besar. Yang mengatakan, tidak ada tipe join fisik yang 'lebih baik' di semua skenario.

Sekarang pertimbangkan kasus 2. Ini tidak melakukan hashing apa pun, tetapi malah melakukan 50000 pencarian terpisah, yang adalah apa yang mendorong pembacaan. Tapi seberapa mahalkah pembacaannya? Orang mungkin mengatakan bahwa jika itu adalah bacaan fisik, itu bisa sangat mahal. Tetapi perlu diingat 1) hanya pembacaan pertama dari halaman yang diberikan dapat fisik, dan 2) meskipun demikian, kasus 1 akan memiliki masalah yang sama atau lebih buruk karena dijamin akan mengenai setiap halaman.

Setiap pencarian membutuhkan menavigasi b-tree ke root, yang secara komputasi mahal dibandingkan dengan probe hash tunggal. Selain itu, pola IO umum untuk sisi bagian dalam loop bersarang bergabung adalah acak, dibandingkan dengan pola akses berurutan input pemindaian sisi-probe untuk hash bergabung. Bergantung pada subsistem IO fisik yang mendasarinya, pembacaan berurutan mungkin lebih cepat dari pembacaan acak. Juga, mekanisme baca-depan SQL Server berfungsi lebih baik dengan IO berurutan, mengeluarkan bacaan yang lebih besar.

Jadi memperhitungkan fakta bahwa kedua kasus harus mengakses setiap halaman setidaknya sekali, tampaknya menjadi pertanyaan mana yang lebih cepat, 1 juta hash atau sekitar 155.000 dibaca terhadap memori? Tes saya tampaknya mengatakan yang terakhir, tetapi SQL Server secara konsisten memilih yang pertama.

Pengoptimal permintaan SQL Server membuat sejumlah asumsi. Salah satunya adalah bahwa akses pertama ke halaman yang dibuat oleh permintaan akan menghasilkan IO fisik ('asumsi cache dingin'). Peluang membaca nanti akan datang dari halaman yang sudah dibaca ke dalam memori oleh kueri yang sama dimodelkan, tetapi ini tidak lebih dari tebakan yang berpendidikan.

Alasan model pengoptimal bekerja dengan cara ini adalah karena umumnya lebih baik untuk mengoptimalkan untuk kasus terburuk (IO fisik diperlukan). Banyak kekurangan dapat ditutupi oleh paralelisme dan menjalankan berbagai hal dalam memori. Rencana kueri yang akan dihasilkan oleh pengoptimal jika diasumsikan semua data dalam memori mungkin berkinerja sangat buruk jika asumsi itu terbukti tidak valid.

Paket yang dibuat dengan menggunakan asumsi cache dingin mungkin tidak berkinerja sebaik jika cache hangat diasumsikan sebagai gantinya, tetapi kinerjanya yang terburuk biasanya akan lebih unggul.

Haruskah saya terus memaksa petunjuk LOOP JOIN ini ketika pengujian menunjukkan hasil semacam ini, atau apakah saya melewatkan sesuatu dalam analisis saya? Saya ragu-ragu untuk menentang pengoptimal SQL Server, tapi rasanya seperti beralih menggunakan hash bergabung jauh lebih awal dari yang seharusnya dalam kasus-kasus seperti ini.

Anda harus sangat berhati-hati dalam melakukan ini karena dua alasan. Pertama, petunjuk bergabung juga secara diam-diam memaksa urutan bergabung fisik untuk cocok dengan urutan tertulis dari kueri (sama seperti jika Anda juga telah menentukan OPTION (FORCE ORDER). Ini sangat membatasi alternatif yang tersedia untuk pengoptimal, dan mungkin tidak selalu seperti yang Anda inginkan. OPTION (LOOP JOIN)Pasukan bersarang loop bergabung untuk kueri, tetapi tidak memberlakukan perintah bergabung tertulis.

Kedua, Anda membuat asumsi bahwa ukuran kumpulan data akan tetap kecil, dan sebagian besar pembacaan logis akan berasal dari cache. Jika asumsi ini menjadi tidak valid (mungkin seiring waktu), kinerja akan menurun. Pengoptimal kueri bawaan cukup bagus dalam bereaksi terhadap perubahan keadaan; menghilangkan kebebasan itu adalah sesuatu yang harus Anda pikirkan dengan keras.

Secara keseluruhan, kecuali ada alasan kuat untuk memaksa loop bergabung, saya akan menghindarinya. Rencana default biasanya cukup dekat dengan optimal, dan cenderung lebih tangguh dalam menghadapi keadaan yang berubah.

Paul White Reinstate Monica
sumber
Paul terima kasih. Analisis detail yang sangat baik. Berdasarkan beberapa pengujian lebih lanjut yang saya lakukan, saya pikir apa yang terjadi adalah bahwa perkiraan terdidik pengoptimal secara konsisten mati untuk contoh khusus ini ketika ukuran tabel temp adalah antara 5K dan 100K. Mengingat fakta bahwa persyaratan kami menjamin tabel temp akan <50K, tampaknya aman bagi saya. Saya ingin tahu, apakah Anda masih menghindari petunjuk bergabung dengan mengetahui hal ini?
JohnnyM
1
@JohnnyM Petunjuk ada karena suatu alasan. Tidak masalah untuk menggunakannya di mana Anda memiliki alasan kuat untuk melakukannya. Yang mengatakan, saya jarang menggunakan petunjuk bergabung karena tersirat FORCE ORDER. Pada kesempatan aneh saya memang menggunakan petunjuk bergabung, saya sering menambahkan OPTION (FORCE ORDER)dengan komentar untuk menjelaskan mengapa.
Paul White Reinstate Monica
0

50.000 baris bergabung dengan tabel sejuta baris tampaknya banyak untuk setiap tabel tanpa indeks.

Sulit untuk memberi tahu Anda apa yang harus dilakukan dalam kasus ini, karena sangat terisolasi dari masalah yang sebenarnya Anda coba selesaikan. Saya tentu berharap bahwa itu bukan pola umum dalam kode Anda di mana Anda bergabung dengan banyak tabel sementara yang tidak diindeks dengan jumlah baris yang signifikan.

Mengambil contoh hanya untuk apa yang dikatakannya, mengapa tidak menaruh indeks pada #Driver? Apakah D.ID benar-benar unik? Jika demikian, itu secara semantik setara dengan pernyataan EXISTS, yang setidaknya akan membuat SQL Server tahu bahwa Anda tidak ingin melanjutkan mencari S untuk nilai duplikat D:

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

Singkatnya, untuk pola ini, saya tidak akan menggunakan petunjuk LOOP. Saya tidak akan menggunakan pola ini. Saya akan melakukan salah satu dari yang berikut ini, dengan urutan prioritas jika feasbile:

  • Gunakan CTE sebagai ganti tabel temp untuk #Driver jika memungkinkan
  • Gunakan indeks nonclustered unik pada #Driver pada ID jika itu unik (dengan asumsi ini adalah satu-satunya saat Anda menggunakan #Driver dan bahwa Anda tidak menginginkan data dari tabel itu sendiri - jika Anda benar-benar membutuhkan data dari tabel itu, Anda mungkin untuk membuatnya menjadi indeks berkerumun)
Dave Markle
sumber