Der Ausführungsplan schlägt vor, dass jede aufeinanderfolgende Schleife mehr Arbeit leistet als die vorherige Schleife. Unter der Annahme, dass die zu löschenden Zeilen gleichmäßig in der Tabelle verteilt sind, muss die erste Schleife etwa 4500 * 221000000/16000000 = 62156 Zeilen scannen, um 4500 zu löschende Zeilen zu finden. Es wird auch die gleiche Anzahl von Clustered-Index-Suchvorgängen für die vendor
Tabelle ausgeführt. Die zweite Schleife muss jedoch über dieselben Zeilen von 62156 - 4500 = 57656 hinaus lesen, die Sie beim ersten Mal nicht gelöscht haben. Es ist zu erwarten, dass die zweite Schleife 120000 Zeilen aus MySourceTable
der vendor
Tabelle scannt und 120000 Suchvorgänge für die Tabelle ausführt . Der Arbeitsaufwand pro Schleife nimmt linear zu. Als Annäherung können wir sagen, dass die durchschnittliche Schleife 102516868 Zeilen von MySourceTable
und lesen muss, um 102516868- Suchvorgänge gegen die durchzuführenvendor
Tabelle. Um 16 Millionen Zeilen mit einer Stapelgröße von 4500 zu löschen, muss Ihr Code 16000000/4500 = 3556 Schleifen ausführen. Der Gesamtaufwand für die Fertigstellung Ihres Codes beträgt also rund 364,5 Milliarden Zeilen, aus MySourceTable
denen gelesen wird, und 364,5 Milliarden Indexsuchen.
Ein kleineres Problem ist, dass Sie eine lokale Variable @BATCHSIZE
in einem TOP-Ausdruck ohne einen RECOMPILE
oder einen anderen Hinweis verwenden. Der Abfrageoptimierer kennt den Wert dieser lokalen Variablen beim Erstellen eines Plans nicht. Es wird davon ausgegangen, dass es gleich 100 ist. In Wirklichkeit löschen Sie 4500 Zeilen anstelle von 100, und aufgrund dieser Diskrepanz könnten Sie möglicherweise einen weniger effizienten Plan erhalten. Die niedrige Kardinalitätsschätzung beim Einfügen in eine Tabelle kann ebenfalls zu einem Leistungseinbruch führen. SQL Server wählt möglicherweise eine andere interne API zum Einfügen aus, wenn es der Meinung ist, dass 100 Zeilen anstelle von 4500 Zeilen eingefügt werden müssen.
Eine Alternative besteht darin, einfach die Primärschlüssel / Clusterschlüssel der Zeilen, die Sie löschen möchten, in eine temporäre Tabelle einzufügen. Abhängig von der Größe Ihrer Schlüsselspalten kann dies leicht in Tempdb passen. In diesem Fall können Sie nur eine minimale Protokollierung erhalten , was bedeutet, dass das Transaktionsprotokoll nicht explodiert. Sie können auch eine minimale Protokollierung für jede Datenbank mit einem Wiederherstellungsmodell von erhalten SIMPLE
. Weitere Informationen zu den Anforderungen finden Sie unter dem Link.
Wenn dies keine Option ist, sollten Sie Ihren Code ändern, damit Sie den Clustered-Index für nutzen können MySourceTable
. Der Schlüssel ist, Ihren Code so zu schreiben, dass Sie ungefähr die gleiche Menge Arbeit pro Schleife erledigen. Sie können dies tun, indem Sie den Index nutzen, anstatt die Tabelle jedes Mal von Anfang an zu scannen. Ich habe einen Blog-Beitrag geschrieben , in dem verschiedene Methoden des Loopings behandelt werden. Die Beispiele in diesem Beitrag fügen zwar in eine Tabelle ein, anstatt sie zu löschen, aber Sie sollten in der Lage sein, den Code anzupassen.
Im folgenden Beispielcode gehe ich davon aus, dass der Primärschlüssel und der Clusterschlüssel von Ihnen MySourceTable
. Ich habe diesen Code ziemlich schnell geschrieben und kann ihn nicht testen:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
DECLARE @BATCHSIZE INT,
@ITERATION INT,
@TOTALROWS INT,
@MSG VARCHAR(500)
@STARTID BIGINT,
@NEXTID BIGINT;
SET DEADLOCK_PRIORITY LOW;
SET @BATCHSIZE = 4500;
SET @ITERATION = 0;
SET @TOTALROWS = 0;
SELECT @STARTID = ID
FROM MySourceTable
ORDER BY ID
OFFSET 0 ROWS
FETCH FIRST 1 ROW ONLY;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
BEGIN TRY
BEGIN TRANSACTION;
WHILE @STARTID IS NOT NULL
BEGIN
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
DELETE FROM MySourceTable_DELCTE
OUTPUT DELETED.*
INTO MyBackupTable
WHERE NOT EXISTS (
SELECT NULL AS Empty
FROM dbo.vendor AS v
WHERE VendorId = v.Id
);
SET @BATCHSIZE = @@ROWCOUNT;
SET @ITERATION = @ITERATION + 1;
SET @TOTALROWS = @TOTALROWS + @BATCHSIZE;
SET @MSG = CAST(GETDATE() AS VARCHAR) + ' Iteration: ' + CAST(@ITERATION AS VARCHAR) + ' Total deletes:' + CAST(@TOTALROWS AS VARCHAR) + ' Next Batch size:' + CAST(@BATCHSIZE AS VARCHAR);
PRINT @MSG;
COMMIT TRANSACTION;
CHECKPOINT;
SET @STARTID = @NEXTID;
SET @NEXTID = NULL;
SELECT @NEXTID = ID
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
OFFSET (60000) ROWS
FETCH FIRST 1 ROW ONLY;
END;
END TRY
BEGIN CATCH
IF @@ERROR <> 0
AND @@TRANCOUNT > 0
BEGIN
PRINT 'There is an error occured. The database update failed.';
ROLLBACK TRANSACTION;
END;
END CATCH;
GO
Der Schlüsselteil ist hier:
WITH MySourceTable_DELCTE AS (
SELECT TOP (60000) *
FROM MySourceTable
WHERE ID >= @STARTID
ORDER BY ID
)
Jede Schleife liest nur 60000 Zeilen aus MySourceTable
. Dies sollte zu einer durchschnittlichen Löschgröße von 4500 Zeilen pro Transaktion und einer maximalen Löschgröße von 60000 Zeilen pro Transaktion führen. Wenn Sie mit einer kleineren Losgröße konservativer sein möchten, ist das auch in Ordnung. Die @STARTID
Variable rückt nach jeder Schleife vor, sodass Sie vermeiden können, dass dieselbe Zeile mehrmals aus der Quelltabelle gelesen wird.