In der Regel empfehle ich aus allen Standardgründen, keine Verknüpfungshinweise zu verwenden. Vor kurzem habe ich jedoch ein Muster gefunden, bei dem ich fast immer eine erzwungene Schleifenverbindung finde, um eine bessere Leistung zu erzielen. Tatsächlich verwende und empfehle ich es so sehr, dass ich eine zweite Meinung einholen wollte, um sicherzugehen, dass mir nichts entgeht. Hier ist ein repräsentatives Szenario (sehr spezifischer Code zum Generieren eines Beispiels ist am Ende):
--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 hat 1 Million Zeilen und seine PK ist ID.
Die temporäre Tabelle #Driver hat nur eine Spalte, ID, keine Indizes und 50 KB Zeilen.
Was ich konsequent finde, ist das Folgende:
Fall 1: NO HINT
Index Scan auf SampleTable
Hash Join
Höhere Dauer (durchschnittlich 333 ms)
Höhere CPU (durchschnittlich 331 ms)
Niedrigere logische Lesezugriffe (4714)
Fall 2: LOOP JOIN HINT
Index Auf SampleTable suchen
Loop Join
Niedrigere Dauer (durchschnittlich 204 ms, 39% weniger)
Niedrigere CPU (durchschnittlich 206, 38% weniger)
Viel höhere logische Lesezugriffe (160015, 34X mehr)
Zunächst erschreckten mich die viel höheren Lesewerte des zweiten Falls ein wenig, da das Verringern der Lesewerte oft als ein anständiges Maß für die Leistung angesehen wird. Aber je mehr ich darüber nachdenke, was tatsächlich passiert, desto weniger geht es mich an. Hier ist mein Denken:
SampleTable ist auf 4714 Seiten enthalten und benötigt ca. 36 MB. Fall 1 scannt sie alle, weshalb wir 4714 Lesungen erhalten. Außerdem muss es 1 Million Hashes ausführen, die viel CPU-Kapazität haben und letztendlich die Zeit proportional verkürzen. Es ist all dieses Hashing, das die Zeit in Fall 1 zu verkürzen scheint.
Betrachten Sie nun Fall 2. Es wird kein Hashing ausgeführt, sondern es werden 50000 separate Suchvorgänge ausgeführt, was die Lesevorgänge beschleunigt. Aber wie teuer sind die Reads im Vergleich? Man könnte sagen, dass dies ziemlich teuer sein kann, wenn es sich um physische Lesevorgänge handelt. Beachten Sie jedoch, dass 1) nur der erste Lesevorgang einer bestimmten Seite physisch sein kann und 2) Fall 1 dennoch dasselbe oder ein noch schlimmeres Problem aufweist, da garantiert wird, dass jede Seite aufgerufen wird.
Berücksichtigt man also die Tatsache, dass beide Fälle mindestens einmal auf jede Seite zugreifen müssen, scheint es eine Frage zu sein, welche schneller ist, 1 Million Hashes oder etwa 155000 Lesevorgänge im Speicher? Meine Tests scheinen das letztere zu sagen, aber SQL Server wählt das erstere konsequent aus.
Frage
Zurück zu meiner Frage: Sollte ich diesen LOOP JOIN-Hinweis weiterhin erzwingen, wenn Tests diese Art von Ergebnissen zeigen, oder fehle ich etwas in meiner Analyse? Ich zögere, gegen den Optimierer von SQL Server vorzugehen, aber es fühlt sich an, als würde er viel früher auf die Verwendung eines Hash-Joins umsteigen, als dies in solchen Fällen der Fall sein sollte.
Update 28.04.2014
Ich habe einige weitere Tests durchgeführt und festgestellt, dass die oben genannten Ergebnisse (auf einer VM mit 2 CPUs) nicht in anderen Umgebungen repliziert werden konnten (ich habe 2 verschiedene physische Maschinen mit 8 und 12 CPUs ausprobiert). In letzteren Fällen schnitt das Optimierungsprogramm viel besser ab, bis es kein derart ausgeprägtes Problem gab. Ich denke, die im Nachhinein naheliegende Lektion ist, dass die Umgebung die Funktionsweise des Optimierers erheblich beeinflussen kann.
Ausführungspläne
Ausführungsplan Fall 1
Ausführungsplan Fall 2

Code zum Generieren eines Beispielfalls
------------------------------------------------------------
-- 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
*/