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 Rencana Eksekusi Kasus 2
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
*/
sumber
FORCE ORDER
. Pada kesempatan aneh saya memang menggunakan petunjuk bergabung, saya sering menambahkanOPTION (FORCE ORDER)
dengan komentar untuk menjelaskan mengapa.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:
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:
sumber