Einzeiliges INSERT… SELECT viel langsamer als separates SELECT

18

Ausgehend von der folgenden Heap-Tabelle mit 400 Zeilen, die von 1 bis 400 nummeriert sind:

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 SELECTAnweisung ist in ungefähr 6 Sekunden abgeschlossen ( Demo , Plan ):

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: @Die OPTIMIZE FORKlausel dient nur dazu, einen Repro in 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 sind abgesehen vom Einfügen einer Zeile identisch.

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

Warum ist die INSERTAussage so viel langsamer?

Paul White sagt GoFundMonica
quelle

Antworten:

21

SQL Server wählt aus, die Heap-Tabellen auf der Innenseite der Schleifen-Joins mithilfe von Sperren auf Zeilenebene zu durchsuchen. Ein vollständiger Scan würde normalerweise 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 günstigste Strategie zu sein scheint.

Die Kardinalitätsfehlschätzung, die absichtlich eingeführt wird OPTIMIZE FOR, indem die Haufen viel häufiger gescannt werden, als der Optimierer erwartet, und es wird keine Spule eingeführt, wie es normalerweise der Fall wäre.

Diese Kombination von Faktoren bedeutet, dass die Leistung sehr stark von der Anzahl der zur Laufzeit erforderlichen Sperren abhängt.

Die SELECTAnweisung profitiert von einer Optimierung, mit der gemeinsam genutzte Sperren auf Zeilenebene übersprungen werden können (nur absichtlich gemeinsam genutzte Sperren auf Seitenebene ), wenn keine Gefahr besteht, nicht festgeschriebene Daten zu lesen, und keine Daten außerhalb der Zeile vorhanden sind.

Die INSERT...SELECTAnweisung profitiert nicht von dieser Optimierung. Daher werden im zweiten Fall jede Sekunde Millionen von RID-Sperren aufgehoben und freigegeben, zusammen mit den absichtlich gemeinsam genutzten Sperren auf Seitenebene.

Die enorme Menge an Sperraktivität 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 das nicht praktisch in dem realen Anwendungsfall ist, die INSERTund SELECTkönnten Aussagen getrennt werden, mit dem Ergebnis , der SELECTin einer Variablen gehalten. Auf diese Weise kann die SELECTAnweisung von der Optimierung zum Überspringen von Sperren profitieren.

Sie können die Isolationsstufe auch ändern, indem Sie entweder keine gemeinsam genutzten Sperren aktivieren oder sicherstellen, dass die Sperren schnell eskalieren.

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

Paul White sagt GoFundMonica
quelle