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
*/
quelle
FORCE ORDER
. Gelegentlich verwende ich einen Join-Hinweis, den ich oftOPTION (FORCE ORDER)
mit einem Kommentar hinzufüge , um zu erklären, warum.50.000 Zeilen, die mit einer Million-Zeilen-Tabelle verknüpft sind, scheinen für jede Tabelle ohne Index eine Menge zu sein.
Es ist schwierig, Ihnen genau zu sagen, was in diesem Fall zu tun ist, da es so isoliert von dem Problem ist, das Sie tatsächlich zu lösen versuchen. Ich hoffe auf jeden Fall, dass es kein allgemeines Muster in Ihrem Code ist, bei dem Sie sich mit vielen nicht indizierten temporären Tabellen mit einer erheblichen Anzahl von Zeilen verbinden.
Nehmen Sie das Beispiel nur für das, was es sagt, warum setzen Sie nicht einfach einen Index auf #Driver? Ist D.ID wirklich einzigartig? In diesem Fall entspricht dies semantisch einer EXISTS-Anweisung, die SQL Server zumindest darüber informiert, dass Sie S nicht weiter nach doppelten Werten von D durchsuchen möchten:
Kurz gesagt, für dieses Muster würde ich keinen LOOP-Hinweis verwenden. Ich würde dieses Muster einfach nicht verwenden. Ich würde in der Reihenfolge der Priorität eine der folgenden Maßnahmen ergreifen, wenn dies möglich ist:
quelle