Eine schlechte Kardinalitätsschätzung disqualifiziert INSERT von der minimalen Protokollierung?

10

Warum ist die zweite INSERTAussage ~ 5x langsamer als die erste?

Aufgrund der Menge der generierten Protokolldaten denke ich, dass die zweite nicht für eine minimale Protokollierung geeignet ist. Die Dokumentation im Leistungshandbuch zum Laden von Daten zeigt jedoch, dass beide Einfügungen minimal protokolliert werden können sollten. Wenn also die minimale Protokollierung der Hauptleistungsunterschied ist, warum qualifiziert sich die zweite Abfrage nicht für die minimale Protokollierung? Was kann getan werden, um die Situation zu verbessern?


Abfrage Nr. 1: Einfügen von 5-MM-Zeilen mit INSERT ... WITH (TABLOCK)

Betrachten Sie die folgende Abfrage, bei der 5-MM-Zeilen in einen Heap eingefügt werden. Diese Abfrage wird ausgeführt 1 secondund generiert 64MBTransaktionsprotokolldaten, wie von gemeldet sys.dm_tran_database_transactions.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Abfrage 2: Einfügen derselben Daten, aber SQL unterschätzt die Anzahl der Zeilen

Betrachten Sie nun diese sehr ähnliche Abfrage, die mit genau denselben Daten arbeitet, aber zufällig aus einer Tabelle (oder einer komplexen SELECTAnweisung mit vielen Verknüpfungen in meinem tatsächlichen Produktionsfall) stammt, in der die Kardinalitätsschätzung zu niedrig ist. Diese Abfrage wird in Transaktionsprotokolldaten ausgeführt 5.5 secondsund generiert diese 461MB.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Vollständiges Skript

In diesem Pastebin finden Sie einen vollständigen Satz von Skripten zum Generieren der Testdaten und zum Ausführen eines dieser Szenarien. Beachten Sie, dass Sie eine Datenbank verwenden müssen, die sich im SIMPLE Wiederherstellungsmodell befindet .


Geschäftlicher Zusammenhang

Wir bewegen uns nur selten in Millionen von Datenzeilen, und es ist wichtig, dass diese Vorgänge sowohl hinsichtlich der Ausführungszeit als auch der Festplatten-E / A-Last so effizient wie möglich sind. Wir hatten anfangs den Eindruck, dass das Erstellen und Verwenden einer Heap-Tabelle INSERT...WITH (TABLOCK)ein guter Weg ist, dies zu tun, sind aber jetzt weniger zuversichtlich geworden, da wir die oben gezeigte Situation in einem tatsächlichen Produktionsszenario beobachtet haben (wenn auch mit komplexeren Abfragen, nicht mit der vereinfachte Version hier).

Geoff Patterson
quelle

Antworten:

6

Warum ist die zweite Abfrage nicht für eine minimale Protokollierung geeignet?

Für die zweite Abfrage ist nur eine minimale Protokollierung verfügbar , die Engine verwendet sie jedoch nicht zur Laufzeit.

Es ist eine Mindestschwelle für die INSERT...SELECTunterhalb der er wählt nicht die Massenlade Optimierungen zu verwenden. Das Einrichten eines Bulk-Rowset-Vorgangs ist mit Kosten verbunden, und das Bulk-Einfügen von nur wenigen Zeilen würde nicht zu einer effizienten Speicherplatznutzung führen.

Was kann getan werden, um die Situation zu verbessern?

Verwenden Sie eine der vielen anderen Methoden (z. B. SELECT INTO), die diesen Schwellenwert nicht haben. Alternativ können Sie die Quellabfrage möglicherweise auf irgendeine Weise neu schreiben, um die geschätzte Anzahl von Zeilen / Seiten über den Schwellenwert für zu erhöhen INSERT...SELECT.

Siehe auch Geoff Selbst Antwort für weitere nützliche Informationen.


Möglicherweise interessante Trivia: SET STATISTICS IO Meldet logische Lesevorgänge für die Zieltabelle nur, wenn keine Massenladeoptimierungen verwendet werden .

Paul White 9
quelle
5

Ich konnte das Problem mit meinem eigenen Prüfstand nachbauen:

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

Dies wirft die Frage auf, warum das Problem nicht durch Aktualisieren der Statistiken in den Quelltabellen "behoben" werden kann, bevor der minimal protokollierte Vorgang ausgeführt wird.

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;
Max Vernon
quelle
2
Im realen Code gibt es eine komplexe SELECTAnweisung mit zahlreichen Verknüpfungen, die die Ergebnismenge für die generiert INSERT. Diese Verknüpfungen führen zu schlechten Kardinalitätsschätzungen für den Operator zum Einfügen der endgültigen Tabelle (den ich im Repro-Skript über den fehlerhaften UPDATE STATISTICSAufruf simuliert habe ), und es ist daher nicht ganz so einfach, einen UPDATE STATISTICSBefehl zur Behebung des Problems auszugeben . Ich stimme voll und ganz zu, dass die Vereinfachung der Abfrage, damit der Kardinalitätsschätzer sie leichter verstehen kann, ein guter Ansatz ist, aber es ist kein Trival, einen Ansatz zu implementieren, wenn eine komplexe Geschäftslogik gegeben ist.
Geoff Patterson
Ich habe keine SQL Server 2014-Instanz, auf der dies getestet werden kann. Das Erkennen von Problemen mit dem neuen Kardinalitätsschätzer von SQL Server 2014 und die Verbesserung von Service Pack 1 sprechen jedoch unter anderem über das Aktivieren des Ablaufverfolgungsflags 4199, um den neuen Kardinalitätsschätzer zu aktivieren. Hast du das versucht?
Max Vernon
Gute Idee, aber es hat nicht geholfen. Ich habe gerade TF 4199, TF 610 (lockert minimale Protokollierungsbedingungen) und beide zusammen ausprobiert (hey, warum nicht?), Aber keine Änderung für die 2. Testabfrage.
Geoff Patterson
3

Schreiben Sie die Quellabfrage auf irgendeine Weise neu, um die geschätzte Anzahl von Zeilen zu erhöhen

Um Pauls Idee zu erweitern, besteht eine Problemumgehung, wenn Sie wirklich verzweifelt sind, darin, eine Dummy-Tabelle hinzuzufügen, die garantiert, dass die geschätzte Anzahl der Zeilen für die Einfügung hoch genug ist, um die Qualität für Optimierungen beim Massenladen zu gewährleisten. Ich habe bestätigt, dass dies zu einer minimalen Protokollierung führt und die Abfrageleistung verbessert.

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

Letzte Imbissbuden

  1. Verwenden Sie SELECT...INTOfür die einmalige Einfügevorgängen wenn minimal Protokollierung erforderlich ist . Wie Paul betont, wird dadurch unabhängig von der Zeilenschätzung eine minimale Protokollierung sichergestellt
  2. Schreiben Sie Abfragen nach Möglichkeit auf einfache Weise, über die der Abfrageoptimierer effektiv nachdenken kann. Es kann beispielsweise möglich sein, eine Abfrage in mehrere Teile aufzuteilen, damit Statistiken auf einer Zwischentabelle erstellt werden können.
  3. Wenn Sie Zugriff auf SQL Server 2014 haben, probieren Sie es in Ihrer Abfrage aus. In meinem eigentlichen Produktionsfall habe ich es gerade ausprobiert und der neue Kardinalitätsschätzer ergab eine viel höhere (und bessere) Schätzung. Die Abfrage wurde dann minimal protokolliert. Dies ist jedoch möglicherweise nicht hilfreich, wenn Sie SQL 2012 und früher unterstützen müssen.
  4. Wenn Sie verzweifelt sind, können Hacky-Lösungen wie diese angewendet werden!

Ein verwandter Artikel

Paul Whites Blogbeitrag Minimal Logging mit INSERT… SELECT in Heap Tables vom Mai 2019 behandelt einige dieser Informationen ausführlicher.

Geoff Patterson
quelle