Warum ist ein temporärer Tisch eine effizientere Lösung für das Halloween-Problem als eine eifrige Spule?

14

Betrachten Sie die folgende Abfrage, mit der Zeilen aus einer Quellentabelle nur dann eingefügt werden, wenn sie nicht bereits in der Zieltabelle enthalten sind:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

Eine mögliche Planform umfasst eine Zusammenführungsverknüpfung und eine eifrige Spule. Der eifrige Spulenoperator ist anwesend, um das Halloween-Problem zu lösen :

erster Plan

Auf meinem Computer wird der obige Code in ca. 6900 ms ausgeführt. Der Repro-Code zum Erstellen der Tabellen befindet sich am Ende der Frage. Wenn ich mit der Leistung unzufrieden bin, kann ich versuchen, die einzufügenden Zeilen in eine temporäre Tabelle zu laden, anstatt mich auf die eifrige Spool zu verlassen. Hier ist eine mögliche Implementierung:

DROP TABLE IF EXISTS #CONSULTANT_RECOMMENDED_TEMP_TABLE;
CREATE TABLE #CONSULTANT_RECOMMENDED_TEMP_TABLE (
    ID BIGINT,
    PRIMARY KEY (ID)
);

INSERT INTO #CONSULTANT_RECOMMENDED_TEMP_TABLE WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1);

Der neue Code wird in ca. 4400 ms ausgeführt. Ich kann aktuelle Pläne abrufen und mithilfe von Actual Time Statistics ™ prüfen, wo auf Bedienerebene Zeit verbracht wird. Beachten Sie, dass das Anfordern eines tatsächlichen Plans zu einem erheblichen Mehraufwand für diese Abfragen führt, sodass die Gesamtsummen nicht mit den vorherigen Ergebnissen übereinstimmen.

╔═════════════╦═════════════╦══════════════╗
  operator    first query  second query 
╠═════════════╬═════════════╬══════════════╣
 big scan     1771         1744         
 little scan  163          166          
 sort         531          530          
 merge join   709          669          
 spool        3202         N/A          
 temp insert  N/A          422          
 temp scan    N/A          187          
 insert       3122         1545         
╚═════════════╩═════════════╩══════════════╝

Der Abfrageplan mit dem eifrigen Spool scheint erheblich mehr Zeit für die Einfüge- und Spool-Operatoren aufzuwenden als der Plan, der die temporäre Tabelle verwendet.

Warum ist der Plan mit der temporären Tabelle effizienter? Ist eine eifrige Spule nicht sowieso meist nur eine interne temporäre Tabelle? Ich glaube, ich suche nach Antworten, die sich auf Interna konzentrieren. Ich kann sehen, wie unterschiedlich die Call-Stacks sind, kann aber das große Ganze nicht verstehen.

Ich bin auf SQL Server 2017 CU 11 für den Fall, dass jemand wissen möchte. Hier ist Code zum Auffüllen der in den obigen Abfragen verwendeten Tabellen:

DROP TABLE IF EXISTS dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR;

CREATE TABLE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR (
ID BIGINT NOT NULL,
PRIMARY KEY (ID)
);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (20000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.A_HEAP_OF_MOSTLY_NEW_ROWS;

CREATE TABLE dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (
ID BIGINT NOT NULL
);

INSERT INTO dbo.A_HEAP_OF_MOSTLY_NEW_ROWS WITH (TABLOCK)
SELECT TOP (1900000) 19999999 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Joe Obbish
quelle

Antworten:

14

Das nenne ich manuellen Halloween-Schutz .

Ein Beispiel für die Verwendung mit einer Update-Anweisung finden Sie in meinem Artikel Optimieren von Update-Abfragen . Man muss ein bisschen vorsichtig sein, um die gleiche Semantik beizubehalten, zum Beispiel indem man die Zieltabelle für alle gleichzeitigen Änderungen sperrt, während die separaten Abfragen ausgeführt werden, falls dies in Ihrem Szenario relevant ist.

Warum ist der Plan mit der temporären Tabelle effizienter? Ist eine eifrige Spule nicht sowieso meist nur eine interne temporäre Tabelle?

Eine Spule hat einige der Merkmale einer temporären Tabelle, aber die beiden sind keine exakten Entsprechungen. Insbesondere ist eine Spule im Wesentlichen eine zeilenweise ungeordnete Einfügung in eine B-Baum-Struktur . Es profitiert von Sperr- und Protokollierungsoptimierungen, unterstützt jedoch keine Massenladeoptimierungen .

Infolgedessen kann die Leistung häufig verbessert werden, indem die Abfrage auf natürliche Weise aufgeteilt wird: Laden Sie die neuen Zeilen massenweise in eine temporäre Tabelle oder Variable, und führen Sie dann eine optimierte Einfügung (ohne expliziten Halloween-Schutz) des temporären Objekts durch.

Wenn Sie diese Trennung vornehmen, können Sie außerdem die Lese- und Schreibabschnitte der ursprünglichen Anweisung separat anpassen.

Als Randnotiz ist es interessant darüber nachzudenken, wie das Halloween-Problem mit Zeilenversionen gelöst werden könnte. Möglicherweise wird eine zukünftige Version von SQL Server diese Funktion unter geeigneten Umständen bereitstellen.


Wie Michael Kutz in einem Kommentar angedeutet hat, könnten Sie auch die Möglichkeit untersuchen, die Lochfüllungsoptimierung zu nutzen, um explizite HP zu vermeiden. Eine Möglichkeit, dies für die Demo zu erreichen, besteht darin, einen eindeutigen Index (gruppiert, wenn Sie möchten) für die IDSpalte von zu erstellen A_HEAP_OF_MOSTLY_NEW_ROWS.

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);

Mit dieser Garantie kann der Optimierer das Füllen von Löchern und das Teilen von Rowsets verwenden:

MERGE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (SERIALIZABLE) AS HICETY
USING dbo.A_HEAP_OF_MOSTLY_NEW_ROWS AS AHOMNR
    ON AHOMNR.ID = HICETY.ID
WHEN NOT MATCHED BY TARGET
THEN INSERT (ID) VALUES (AHOMNR.ID);

MERGE-Plan

Obwohl es interessant ist, können Sie in vielen Fällen dennoch eine bessere Leistung erzielen, indem Sie sorgfältig implementierten manuellen Halloween-Schutz anwenden.

Paul White Monica wieder einsetzen
quelle
5

Um die Antwort von Paul ein wenig zu erweitern, scheint ein Teil des Unterschieds in der verstrichenen Zeit zwischen dem Spool- und dem Temp-Table-Ansatz auf den Mangel an Unterstützung für die DML Request SortOption im Spool-Plan zurückzuführen zu sein. Mit dem undokumentierten Ablaufverfolgungsflag 8795 springt die verstrichene Zeit für den Temp-Table-Ansatz von 4400 ms auf 5600 ms.

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1, QUERYTRACEON 8795);

Beachten Sie, dass dies nicht genau dem Einfügen entspricht, das vom Spool-Plan ausgeführt wird. Diese Abfrage schreibt erheblich mehr Daten in das Transaktionsprotokoll.

Der gleiche Effekt kann mit einigen Tricks umgekehrt gesehen werden. Es ist möglich, SQL Server zu ermutigen, eine Sortierung anstelle einer Spool für Halloween Protection zu verwenden. Eine Implementierung:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (987654321) 
maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
ORDER BY maybe_new_rows.ID, maybe_new_rows.ID + 1
OPTION (MAXDOP 1, QUERYTRACEON 7470, MERGE JOIN);

Jetzt hat der Plan einen TOP N-Sortieroperator anstelle der Spule. Die Sortierung ist ein blockierender Operator, sodass die Spule nicht mehr benötigt wird:

Bildbeschreibung hier eingeben

Vor allem haben wir jetzt Unterstützung für die DML Request SortOption. Bei erneuter Betrachtung der tatsächlichen Zeitstatistik benötigt der Einfügeoperator nur noch 1623 ms. Die Ausführung des gesamten Plans dauert ca. 5400 ms, ohne dass ein tatsächlicher Plan angefordert wird.

Wie Hugo erklärt , sorgt der Eager Spool-Operator für Ordnung. Das ist am einfachsten mit einem TOP PERCENTPlan zu erkennen . Es ist bedauerlich, dass die ursprüngliche Abfrage mit dem Spool die sortierte Natur der Daten im Spool nicht besser ausnutzen kann.

Joe Obbish
quelle