Sollte ich in SQL Server im folgenden Fall einen LOOP JOIN erzwingen?

15

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 Plan 1 Ausführungsplan Fall 2 Bildbeschreibung hier eingeben

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

*/
JohnnyM
quelle

Antworten:

13

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.

Für einen Hash-Join fallen Startkosten an (das Erstellen der Hash-Tabelle, bei der es sich auch um einen Blockierungsvorgang handelt), aber der Hash-Join hat letztendlich die niedrigsten theoretischen Kosten pro Zeile für die drei von SQL Server unterstützten physischen Join-Typen, und zwar in Begriffe von IO und CPU. Hash-Join kommt mit einer relativ kleinen Build-Eingabe und einer großen Probe-Eingabe besonders gut zur Geltung. Allerdings ist kein physischer Verknüpfungstyp in allen Szenarien "besser".

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 Lesevorgänge 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.

Jede Suche erfordert das Navigieren eines B-Baums zur Wurzel, was im Vergleich zu einer einzelnen Hash-Sonde rechenintensiv ist. Darüber hinaus ist das allgemeine E / A-Muster für die Innenseite eines Joins mit verschachtelten Schleifen zufällig, verglichen mit dem sequentiellen Zugriffsmuster der tastkopfseitigen Scan-Eingabe für einen Hash-Join. Abhängig vom zugrunde liegenden physischen E / A-Subsystem sind sequentielle Lesevorgänge möglicherweise schneller als zufällige Lesevorgänge. Außerdem funktioniert der SQL Server-Vorauslesemechanismus bei sequenziellen E / A-Vorgängen besser und gibt größere Lesevorgänge aus.

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.

Das SQL Server-Abfrageoptimierungsprogramm nimmt eine Reihe von Annahmen an. Zum einen führt der erste Zugriff auf eine durch eine Abfrage erstellte Seite zu einer physischen E / A (der so genannten Cold-Cache-Annahme). Die Wahrscheinlichkeit, dass ein späterer Lesevorgang von einer Seite kommt, die bereits von derselben Abfrage in den Speicher gelesen wurde, wird modelliert, dies ist jedoch nur eine begründete Vermutung.

Der Grund, warum das Modell des Optimierers auf diese Weise funktioniert, ist, dass es im Allgemeinen besser ist, für den ungünstigsten Fall zu optimieren (physische E / A ist erforderlich). Viele Mängel können durch Parallelität und das Ausführen von Dingen im Gedächtnis verdeckt werden. Die Abfragepläne, die das Optimierungsprogramm erstellen würde, wenn angenommen wird, dass sich alle Daten im Speicher befinden, sind möglicherweise sehr schlecht, wenn sich diese Annahme als ungültig herausstellt.

Der Plan, der unter Verwendung der Cold-Cache-Annahme erstellt wurde, funktioniert möglicherweise nicht so gut, als wenn stattdessen ein Warm-Cache angenommen würde. Im schlimmsten Fall ist die Leistung jedoch in der Regel besser.

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.

Sie sollten aus zwei Gründen sehr vorsichtig damit sein. Erstens erzwingen Verknüpfungshinweise im Hintergrund, dass die physische Verknüpfungsreihenfolge mit der schriftlichen Reihenfolge der Abfrage übereinstimmt (genau so, als ob Sie dies ebenfalls angegeben hätten OPTION (FORCE ORDER). Dies schränkt die für das Optimierungsprogramm verfügbaren Alternativen erheblich ein und ist möglicherweise nicht immer das, was Sie möchten. OPTION (LOOP JOIN)Erzwingt verschachtelte Schleifen verbindet sich für die Abfrage, erzwingt jedoch nicht die schriftliche Verknüpfungsreihenfolge.

Zweitens gehen Sie davon aus, dass die Datenmenge klein bleibt und die meisten logischen Lesevorgänge aus dem Cache stammen. Wenn diese Annahmen ungültig werden (möglicherweise im Laufe der Zeit), wird die Leistung beeinträchtigt. Das integrierte Abfrageoptimierungsprogramm reagiert sehr gut auf sich ändernde Umstände. Das Entfernen dieser Freiheit ist etwas, über das Sie nachdenken sollten.

Insgesamt würde ich es vermeiden, es sei denn, es gibt einen zwingenden Grund, Loops-Joins zu erzwingen. Die Standardpläne sind normalerweise nahezu optimal und sind in der Regel widerstandsfähiger, wenn sich die Umstände ändern.

Paul White Monica wieder einsetzen
quelle
Vielen Dank, Paul. Hervorragende detaillierte Analyse. Basierend auf einigen weiteren Tests, die ich durchgeführt habe, ist das, was passiert, meiner Meinung nach, dass die fundierten Vermutungen des Optimierers für dieses spezielle Beispiel beständig falsch sind, wenn die Größe der temporären Tabelle zwischen 5 KB und 100 KB liegt. Angesichts der Tatsache, dass unsere Anforderungen garantieren, dass die temporäre Tabelle <50.000 ist, scheint es mir sicher zu sein. Ich bin neugierig, würden Sie trotzdem vermeiden, dass Sie dies wissen?
JohnnyM
1
@ JohnnyM Hinweise gibt es aus einem Grund. Es ist in Ordnung, sie dort zu verwenden, wo Sie gute Gründe dafür haben. Trotzdem verwende ich aufgrund des Implizierten selten Join- Hinweise FORCE ORDER. Gelegentlich verwende ich einen Join-Hinweis, den ich oft OPTION (FORCE ORDER)mit einem Kommentar hinzufüge , um zu erklären, warum.
Paul White Reinstate Monica
0

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:

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

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:

  • Verwenden Sie für #Driver nach Möglichkeit einen CTE anstelle einer temporären Tabelle
  • Verwenden Sie einen eindeutigen Nonclustered-Index für #Driver on ID, wenn dieser eindeutig ist (vorausgesetzt, Sie verwenden #Driver nur in diesem Fall und möchten keine Daten aus der Tabelle selbst - wenn Sie tatsächlich Daten aus dieser Tabelle benötigen, müssen Sie möglicherweise zu gut, um daraus einen Clustered-Index zu machen)
Dave Markle
quelle