SQL Server: Aktualisieren von Feldern auf großen Tabellen in kleinen Blöcken: Wie erhalte ich Fortschritt / Status?

10

Wir haben eine sehr große Tabelle (100 Millionen Zeilen) und müssen einige Felder aktualisieren.

Für den Protokollversand usw. möchten wir natürlich auch Transaktionen mit mundgerechter Größe beibehalten.

  • Wird das Folgende den Trick machen?
  • Und wie können wir es dazu bringen, eine Ausgabe zu drucken, damit wir Fortschritte sehen können? (Wir haben versucht, dort eine PRINT-Anweisung hinzuzufügen, aber während der while-Schleife wurde nichts ausgegeben.)

Der Code lautet:

DECLARE @CHUNK_SIZE int
SET @CHUNK_SIZE = 10000

UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
where deleted is null or deletedDate is null

WHILE @@ROWCOUNT > 0
BEGIN
    UPDATE TOP(@CHUNK_SIZE) [huge-table] set deleted = 0, deletedDate = '2000-01-01'
    where deleted is null or deletedDate is null
END
Jonesome stellt Monica wieder her
quelle

Antworten:

12

Diese Frage war mir nicht bekannt, als ich die zugehörige Frage beantwortete ( Sind in dieser while-Schleife explizite Transaktionen erforderlich? ). Der Vollständigkeit halber werde ich dieses Problem hier ansprechen, da es nicht Teil meines Vorschlags in dieser verknüpften Antwort war .

Da ich vorschlage, dies über einen SQL Agent-Job zu planen (es sind immerhin 100 Millionen Zeilen), denke ich nicht, dass irgendeine Form des Sendens von Statusnachrichten an den Client (dh SSMS) ideal ist (wenn dies der Fall ist) Immer wenn andere Projekte benötigt werden, stimme ich Vladimir zu, dass die Verwendung RAISERROR('', 10, 1) WITH NOWAIT;der richtige Weg ist.

In diesem speziellen Fall würde ich eine Statustabelle erstellen, die pro Schleife mit der Anzahl der bisher aktualisierten Zeilen aktualisiert werden kann. Und es tut nicht weh, in der aktuellen Zeit einen Herzschlag auf den Prozess zu werfen.

Vorausgesetzt, Sie möchten den Vorgang abbrechen und neu starten können, Ich bin es leid, das UPDATE der Haupttabelle mit dem UPDATE der Statustabelle in eine explizite Transaktion zu verpacken. Wenn Sie jedoch das Gefühl haben, dass die Statustabelle aufgrund des Abbruchs nicht mehr synchron ist, können Sie den aktuellen Wert leicht aktualisieren, indem Sie ihn einfach manuell mit dem aktualisieren COUNT(*) FROM [huge-table] WHERE deleted IS NOT NULL AND deletedDate IS NOT NULL.und es gibt zwei Tabellen zum UPDATE (dh die Haupttabelle und die Statustabelle). Wir sollten eine explizite Transaktion verwenden, um diese beiden Tabellen synchron zu halten. Wir möchten jedoch nicht riskieren, eine verwaiste Transaktion zu haben, wenn Sie den Prozess bei a abbrechen Punkt, nachdem die Transaktion gestartet, aber nicht festgeschrieben wurde. Dies sollte sicher sein, solange Sie den SQL Agent-Job nicht stoppen.

Wie können Sie den Prozess stoppen, ohne ihn zu stoppen? Indem ich ihn auffordere aufzuhören :-). Ja. Indem Sie dem Prozess ein "Signal" senden (ähnlich wie kill -3in Unix), können Sie anfordern, dass er im nächsten geeigneten Moment stoppt (dh wenn keine aktive Transaktion vorhanden ist!) Und sich selbst schön und ordentlich bereinigen lässt.

Wie können Sie in einer anderen Sitzung mit dem laufenden Prozess kommunizieren? Verwenden Sie denselben Mechanismus, den wir erstellt haben, um Ihnen den aktuellen Status mitzuteilen: die Statustabelle. Wir müssen nur eine Spalte hinzufügen, die der Prozess am Anfang jeder Schleife überprüft, damit er weiß, ob er fortfahren oder abbrechen soll. Und da dies als SQL Agent-Job geplant werden soll (alle 10 oder 20 Minuten ausgeführt), sollten wir dies auch ganz am Anfang überprüfen, da es keinen Sinn macht, eine temporäre Tabelle mit 1 Million Zeilen zu füllen, wenn der Prozess gerade läuft einen Moment später zu beenden und keine dieser Daten zu verwenden.

DECLARE @BatchRows INT = 1000000,
        @UpdateRows INT = 4995;

IF (OBJECT_ID(N'dbo.HugeTable_TempStatus') IS NULL)
BEGIN
  CREATE TABLE dbo.HugeTable_TempStatus
  (
    RowsUpdated INT NOT NULL, -- updated by the process
    LastUpdatedOn DATETIME NOT NULL, -- updated by the process
    PauseProcess BIT NOT NULL -- read by the process
  );

  INSERT INTO dbo.HugeTable_TempStatus (RowsUpdated, LastUpdatedOn, PauseProcess)
  VALUES (0, GETDATE(), 0);
END;

-- First check to see if we should run. If no, don't waste time filling temp table
IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
BEGIN
  PRINT 'Process is paused. No need to start.';
  RETURN;
END;

CREATE TABLE #FullSet (KeyField1 DataType1, KeyField2 DataType2);
CREATE TABLE #CurrentSet (KeyField1 DataType1, KeyField2 DataType2);

INSERT INTO #FullSet (KeyField1, KeyField2)
  SELECT TOP (@BatchRows) ht.KeyField1, ht.KeyField2
  FROM   dbo.HugeTable ht
  WHERE  ht.deleted IS NULL
  OR     ht.deletedDate IS NULL

WHILE (1 = 1)
BEGIN
  -- Check if process is paused. If yes, just exit cleanly.
  IF (EXISTS(SELECT * FROM dbo.HugeTable_TempStatus WHERE PauseProcess = 1))
  BEGIN
    PRINT 'Process is paused. Exiting.';
    BREAK;
  END;

  -- grab a set of rows to update
  DELETE TOP (@UpdateRows)
  FROM   #FullSet
  OUTPUT Deleted.KeyField1, Deleted.KeyField2
  INTO   #CurrentSet (KeyField1, KeyField2);

  IF (@@ROWCOUNT = 0)
  BEGIN
    RAISERROR(N'All rows have been updated!!', 16, 1);
    BREAK;
  END;

  BEGIN TRY
    BEGIN TRAN;

    -- do the update of the main table
    UPDATE ht
    SET    ht.deleted = 0,
           ht.deletedDate = '2000-01-01'
    FROM   dbo.HugeTable ht
    INNER JOIN #CurrentSet cs
            ON cs.KeyField1 = ht.KeyField1
           AND cs.KeyField2 = ht.KeyField2;

    -- update the current status
    UPDATE ts
    SET    ts.RowsUpdated += @@ROWCOUNT,
           ts.LastUpdatedOn = GETDATE()
    FROM   dbo.HugeTable_TempStatus ts;

    COMMIT TRAN;
  END TRY
  BEGIN CATCH
    IF (@@TRANCOUNT > 0)
    BEGIN
      ROLLBACK TRAN;
    END;

    THROW; -- raise the error and terminate the process
  END CATCH;

  -- clear out rows to update for next iteration
  TRUNCATE TABLE #CurrentSet;

  WAITFOR DELAY '00:00:01'; -- 1 second delay for some breathing room
END;

-- clean up temp tables when testing
-- DROP TABLE #FullSet; 
-- DROP TABLE #CurrentSet; 

Sie können den Status dann jederzeit mit der folgenden Abfrage überprüfen:

SELECT sp.[rows] AS [TotalRowsInTable],
       ts.RowsUpdated,
       (sp.[rows] - ts.RowsUpdated) AS [RowsRemaining],
       ts.LastUpdatedOn
FROM sys.partitions sp
CROSS JOIN dbo.HugeTable_TempStatus ts
WHERE  sp.[object_id] = OBJECT_ID(N'ResizeTest')
AND    sp.[index_id] < 2;

Möchten Sie den Prozess anhalten, unabhängig davon, ob er in einem SQL Agent-Job oder sogar in SSMS auf dem Computer eines anderen ausgeführt wird? Lauf einfach:

UPDATE ht
SET    ht.PauseProcess = 1
FROM   dbo.HugeTable_TempStatus ts;

Möchten Sie, dass der Prozess erneut gestartet werden kann? Lauf einfach:

UPDATE ht
SET    ht.PauseProcess = 0
FROM   dbo.HugeTable_TempStatus ts;

AKTUALISIEREN:

Hier sind einige zusätzliche Dinge, die Sie versuchen sollten, um die Leistung dieses Vorgangs zu verbessern. Keiner hilft garantiert, aber es lohnt sich wahrscheinlich, ihn auszuprobieren. Und mit 100 Millionen zu aktualisierenden Zeilen haben Sie genügend Zeit / Gelegenheit, einige Variationen zu testen ;-).

  1. Fügen Sie TOP (@UpdateRows)der UPDATE-Abfrage hinzu, damit die oberste Zeile wie folgt aussieht:
    UPDATE TOP (@UpdateRows) ht
    Manchmal hilft es dem Optimierer zu wissen, wie viele Zeilen maximal betroffen sind, damit keine Zeit damit verschwendet wird, nach mehr zu suchen.
  2. Fügen Sie der #CurrentSettemporären Tabelle einen PRIMARY KEY hinzu . Die Idee hier ist, dem Optimierer beim JOIN zur 100-Millionen-Zeilentabelle zu helfen.

    Und nur um es so anzugeben, dass es nicht mehrdeutig ist, sollte es keinen Grund geben, der #FullSettemporären Tabelle eine PK hinzuzufügen, da es sich nur um eine einfache Warteschlangentabelle handelt, bei der die Reihenfolge irrelevant ist.

  3. In einigen Fällen ist es hilfreich, einen gefilterten Index hinzuzufügen, um die SELECTEinspeisung in die #FullSettemporäre Tabelle zu unterstützen. Hier einige Überlegungen zum Hinzufügen eines solchen Index:
    1. Die WHERE-Bedingung sollte daher mit der WHERE-Bedingung Ihrer Abfrage übereinstimmen WHERE deleted is null or deletedDate is null
    2. Zu Beginn des Prozesses stimmen die meisten Zeilen mit Ihrer WHERE-Bedingung überein, sodass ein Index nicht so hilfreich ist. Möglicherweise möchten Sie warten, bis die 50% -Marke erreicht ist, bevor Sie diese hinzufügen. Wie viel es hilft und wann es am besten ist, den Index hinzuzufügen, hängt natürlich von mehreren Faktoren ab. Es ist also ein bisschen Versuch und Irrtum.
    3. Möglicherweise müssen Sie STATS manuell AKTUALISIEREN und / oder den Index neu erstellen, um ihn auf dem neuesten Stand zu halten, da sich die Basisdaten häufig ändern
    4. Denken Sie daran, dass der Index, während er dem hilft SELECT, das verletzt, UPDATEda es sich um ein anderes Objekt handelt, das während dieses Vorgangs aktualisiert werden muss, daher mehr E / A. Dies funktioniert sowohl bei der Verwendung eines gefilterten Index (der beim Aktualisieren von Zeilen kleiner wird, da weniger Zeilen mit dem Filter übereinstimmen) als auch beim Warten auf das Hinzufügen des Index (wenn dies am Anfang nicht besonders hilfreich sein wird, besteht kein Grund, dies zu tun die zusätzliche E / A).
Solomon Rutzky
quelle
1
Das ist ausgezeichnet. Ich leite es jetzt und es raucht, dass wir es tagsüber online laufen lassen können. Vielen Dank!
Jonesome Reinstate Monica
@samsmith Bitte lesen Sie den Abschnitt UPDATE, den ich gerade hinzugefügt habe, da es einige Ideen gibt, wie der Prozess möglicherweise noch schneller ausgeführt werden kann.
Solomon Rutzky
Ohne die UPDATE-Verbesserungen erhalten wir ungefähr 8 Millionen Updates / Stunde ... mit @BatchRows auf 10000000 (zehn Millionen)
Jonesome Reinstate Monica
@samsmith Das ist großartig :) richtig? Beachten Sie zwei Dinge: 1) Der Prozess wird langsamer, da immer weniger Zeilen mit der WHERE-Klausel übereinstimmen. Daher ist es ein guter Zeitpunkt, einen gefilterten Index hinzuzufügen, aber Sie haben bereits einen nicht gefilterten Index am hinzugefügt Ich bin mir nicht sicher, ob dies helfen oder schaden wird, aber ich würde trotzdem erwarten, dass der Durchsatz abnimmt, wenn er sich dem Ziel nähert, und 2) Sie könnten den Durchsatz erhöhen, indem Sie ihn WAITFOR DELAYauf etwa eine halbe Sekunde reduzieren. Dies ist jedoch ein Kompromiss zwischen Parallelität und möglicherweise der Überweisung per Protokollversand.
Solomon Rutzky
Wir sind mit 8 Millionen Zeilen / Stunde zufrieden. Ja, wir können sehen, dass es langsamer wird. Wir zögern, weitere Indizes zu erstellen (da die Tabelle für den gesamten Build gesperrt ist). Was wir ein paar Mal gemacht haben, ist eine Neuordnung des vorhandenen Index (weil das online ist).
Jonesome Reinstate Monica
4

Beantwortung des zweiten Teils: Drucken einer Ausgabe während der Schleife.

Ich habe einige lang laufende Wartungsverfahren, die der Systemadministrator manchmal ausführen muss.

Ich habe sie von SSMS ausgeführt und festgestellt, dass die PRINTAnweisung in SSMS erst angezeigt wird, nachdem die gesamte Prozedur abgeschlossen ist.

Also benutze ich RAISERRORmit geringem Schweregrad:

DECLARE @VarTemp nvarchar(32);
SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;

Ich verwende SQL Server 2008 Standard und SSMS 2012 (11.0.3128.0). Hier ist ein vollständiges Arbeitsbeispiel für die Ausführung in SSMS:

DECLARE @VarCount int = 0;
DECLARE @VarTemp nvarchar(32);

WHILE @VarCount < 3
BEGIN
    SET @VarTemp = CONVERT(nvarchar(32), GETDATE(), 121);
    --RAISERROR (N'Your message. Current time is %s.', 0, 1, @VarTemp) WITH NOWAIT;
    --PRINT @VarTemp;

    WAITFOR DELAY '00:00:02';
    SET @VarCount = @VarCount + 1;
END

Wenn ich auskommentiere RAISERRORund nur PRINTdie Nachrichten auf der Registerkarte Nachrichten in SSMS hinterlasse, werden sie nach 6 Sekunden erst nach Abschluss des gesamten Stapels angezeigt.

Wenn ich die Nachrichten auf der Registerkarte Nachrichten in SSMS auskommentiere PRINTund verwende, werden sie RAISERRORangezeigt, ohne 6 Sekunden zu warten, aber während die Schleife fortschreitet.

Interessanterweise sehe ich beide Nachrichten , wenn ich beide RAISERRORund verwende PRINT. Zuerst kommt die Nachricht von zuerst RAISERROR, dann Verzögerung für 2 Sekunden, dann zuerst PRINTund zweitens RAISERRORund so weiter.


In anderen Fällen verwende ich eine separate dedizierte logTabelle und füge einfach eine Zeile mit einigen Informationen in die Tabelle ein, die den aktuellen Status und den Zeitstempel des lang laufenden Prozesses beschreiben.

Während der lange Prozess läuft ich regelmäßig SELECTvon der logTabelle, um zu sehen, was los ist.

Dies hat natürlich einen gewissen Overhead, hinterlässt jedoch ein Protokoll (oder einen Protokollverlauf), das ich später in meinem eigenen Tempo untersuchen kann.

Vladimir Baranov
quelle
In SQL 2008/2014 können wir keine Ergebnisse von Raiseerror sehen. Was fehlt uns?
Jonesome Reinstate Monica
@samsmith, ich habe ein vollständiges Beispiel hinzugefügt. Versuch es. Welches Verhalten erhalten Sie in diesem einfachen Beispiel?
Vladimir Baranov
2

Sie können es von einer anderen Verbindung aus überwachen, indem Sie Folgendes tun:

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT COUNT(*) FROM [huge-table] WHERE deleted IS NULL OR deletedDate IS NULL 

um zu sehen, wie viel noch zu tun ist. Dies kann nützlich sein, wenn eine Anwendung den Prozess aufruft, anstatt ihn manuell in SSMS oder ähnlichem auszuführen, und den Fortschritt anzeigen muss: Führen Sie den Hauptprozess asynchron (oder in einem anderen Thread) aus und rufen Sie dann in einer Schleife auf, wie viel noch übrig ist "Überprüfen Sie jede Zeit, bis der asynchrone Aufruf (oder Thread) abgeschlossen ist.

Wenn Sie die Isolationsstufe so locker wie möglich einstellen, sollte diese in angemessener Zeit zurückkehren, ohne aufgrund von Sperrproblemen hinter der Haupttransaktion hängen zu bleiben. Dies könnte natürlich bedeuten, dass der zurückgegebene Wert etwas ungenau ist, aber als einfache Fortschrittsanzeige sollte dies überhaupt keine Rolle spielen.

David Spillett
quelle