it-swarm.com.de

Einzeiliges INSERT ... SELECT viel langsamer als separates SELECT

Angesichts der folgenden Heap-Tabelle mit 400 Zeilen von 1 bis 400:

DROP TABLE IF EXISTS dbo.N;
GO
SELECT 
    SV.number
INTO dbo.N 
FROM master.dbo.spt_values AS SV
WHERE 
    SV.[type] = N'P'
    AND SV.number BETWEEN 1 AND 400;

und die folgenden Einstellungen:

SET NOCOUNT ON;
SET STATISTICS IO, TIME OFF;
SET STATISTICS XML OFF;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

Die folgende Anweisung SELECT wird in ungefähr 6 Sekunden ( Demo , Plan abgeschlossen ):

DECLARE @n integer = 400;

SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Hinweis: @The OPTIMIZE FOR Klausel dient nur dazu, einen Repro von vernünftiger Größe zu erstellen, der die wesentlichen Details des eigentlichen Problems erfasst, einschließlich einer Kardinalitätsfehlschätzung, die aus verschiedenen Gründen auftreten kann.

Wenn die einzeilige Ausgabe in eine Tabelle geschrieben wird, dauert es 19 Sekunden ( Demo , Plan ):

DECLARE @T table (c bigint NOT NULL);

DECLARE @n integer = 400;

INSERT @T
    (c)
SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Die Ausführungspläne erscheinen bis auf das Einfügen einer Zeile identisch.

Die gesamte zusätzliche Zeit scheint von der CPU-Auslastung verbraucht zu werden.

Warum ist die Anweisung INSERT so viel langsamer?

18
Paul White 9

SQL Server scannt die Heap-Tabellen auf der Innenseite der Schleifenverknüpfungen mithilfe von Sperren auf Zeilenebene. Ein vollständiger Scan würde normalerweise das Sperren auf Seitenebene wählen, aber eine Kombination aus der Größe der Tabelle und dem Prädikat bedeutet, dass die Speicher-Engine Zeilensperren wählt, da dies die billigste Strategie zu sein scheint.

Die durch OPTIMIZE FOR Absichtlich eingeführte Kardinalitätsfehlschätzung bedeutet, dass die Heaps viele mehrmals gescannt werden, als der Optimierer erwartet, und es wird keine Spule eingeführt, wie dies normalerweise der Fall wäre.

Diese Kombination von Faktoren bedeutet, dass die Leistung sehr empfindlich auf die Anzahl der zur Laufzeit erforderlichen Sperren reagiert.

Die Anweisung SELECT profitiert von einer Optimierung, mit der gemeinsam genutzte Sperren auf Zeilenebene übersprungen werden können (nur absichtlich gesperrte Sperren auf Seitenebene verwendet werden), wenn keine Gefahr besteht, nicht festgeschriebene Daten zu lesen. und es gibt keine Off-Row-Daten.

Die Anweisung INSERT...SELECT Profitiert nicht von dieser Optimierung, sodass im zweiten Fall jede Sekunde Millionen von RID-Sperren genommen und freigegeben werden, zusammen mit den absichtlich geteilten Sperren auf Seitenebene.

Die enorme Menge an Sperraktivitäten erklärt die zusätzliche CPU und die verstrichene Zeit.

Die natürlichste Problemumgehung besteht darin, sicherzustellen, dass der Optimierer (und die Speicher-Engine) angemessene Kardinalitätsschätzungen erhalten, damit sie gute Entscheidungen treffen können.

Wenn dies im realen Anwendungsfall nicht praktikabel ist, können die Anweisungen INSERT und SELECT getrennt werden, wobei das Ergebnis von SELECT in einer Variablen enthalten ist. Dadurch kann die Anweisung SELECT von der Optimierung des Überspringens von Sperren profitieren.

Das Ändern der Isolationsstufe kann auch funktionieren, indem entweder keine gemeinsam genutzten Sperren verwendet werden oder indem sichergestellt wird, dass die Sperreneskalation schnell erfolgt.

Als letzter Punkt von Interesse kann die Abfrage noch schneller als der optimierte Fall SELECT ausgeführt werden, indem die Verwendung von Spools unter Verwendung des undokumentierten Ablaufverfolgungsflags 8691 erzwungen wird.

21
Paul White 9