it-swarm.com.de

SQL-Update-Anweisung, die sehr lange dauert / stundenlang hohe Festplattenauslastung

Ja, es klingt nach einem sehr allgemeinen Problem, aber ich konnte es noch nicht sehr eingrenzen.

Ich habe also eine UPDATE-Anweisung in einer SQL-Batchdatei:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID

B hat 40.000 Datensätze, A hat 4 Millionen Datensätze und sie sind über A.B_ID 1 zu n miteinander verbunden, obwohl zwischen den beiden kein FK besteht.

Grundsätzlich berechne ich ein Feld für Data Mining-Zwecke vor. Obwohl ich den Namen der Tabellen für diese Frage geändert habe, habe ich die Anweisung nicht geändert, es ist wirklich so einfach.

Die Ausführung dauert Stunden, daher habe ich beschlossen, alles abzubrechen. Die Datenbank wurde beschädigt, daher habe ich sie gelöscht, eine Sicherung wiederhergestellt, die ich kurz vor dem Ausführen der Anweisung erstellt habe, und beschlossen, mit einem Cursor näher auf Details einzugehen:

DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB 
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id

WHILE @@FETCH_STATUS = 0
BEGIN
    DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
    RAISERROR(@Msg, 10, 1) WITH NOWAIT

    UPDATE A
    SET A.X = B.X
    FROM A JOIN B ON A.B_ID = B.ID
    WHERE B.ID = @Id

    FETCH NEXT FROM CursorB INTO @Id
END

Jetzt kann ich sehen, dass es mit einer Nachricht mit absteigender ID ausgeführt wird. Es dauert ungefähr 5 Minuten, um von id = 40k auf id = 13 zu gelangen

Und dann, bei ID 13, scheint es aus irgendeinem Grund zu hängen. Die Datenbank hat außer SSMS keine Verbindung zu ihr, aber sie hängt nicht wirklich:

  • die Festplatte läuft ununterbrochen, also macht sie definitiv etwas (ich habe im Prozess-Explorer überprüft, dass es sich tatsächlich um den Prozess sqlserver.exe handelt, der sie verwendet).
  • Ich habe sp_who2 ausgeführt, die SPID (70) der SUSPENDED-Sitzung gefunden und dann das folgende Skript ausgeführt:

    wählen Sie * aus sys.dm_exec_requests aus. r Join sys.dm_os_tasks t on r.session_id = t.session_id wobei r.session_id = 70

Dies gibt mir den wait_type, der die meiste Zeit PAGEIOLATCH_SH ist, aber manchmal tatsächlich zu WRITE_COMPLETION wechselt, was vermutlich passiert, wenn das Protokoll geleert wird

  • die Protokolldatei, die 1,6 GB groß war, als ich die Datenbank wiederherstellte (und ID 13 erreichte), ist jetzt 3,5 GB groß

Andere vielleicht nützliche Informationen:

  • die Anzahl der Datensätze in Tabelle A für B_ID 13 ist nicht groß (14).
  • Meine Kollegin hat nicht das gleiche Problem auf ihrem Computer, mit einer Kopie dieser Datenbank (von vor ein paar Monaten) mit der gleichen Struktur.
  • tabelle A ist mit Abstand die größte Tabelle in der DB
  • Es hat mehrere Indizes, und mehrere indizierte Ansichten verwenden es.
  • Es gibt keinen anderen Benutzer in der Datenbank, sie ist lokal und wird von keiner Anwendung verwendet.
  • Die LDF-Datei ist nicht in der Größe beschränkt.
  • Das Wiederherstellungsmodell ist EINFACH, die Kompatibilitätsstufe ist 100
  • Procmon gibt mir nicht viele Informationen: sqlserver.exe liest und schreibt viel aus den Dateien MDF und LDF).

Ich warte immer noch darauf, dass es fertig ist (es ist 1h30), aber ich hatte gehofft, dass mir vielleicht jemand eine andere Aktion geben würde, mit der ich versuchen könnte, dieses Problem zu beheben.

Bearbeitet: Extrakt aus dem Procmon-Protokoll hinzufügen

15:24:02.0506105    sqlservr.exe    1760    ReadFile    C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF    SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal

Bei Verwendung von DBCC PAGE scheint es, Felder zu lesen und in Felder zu schreiben, die wie Tabellen A (oder einen ihrer Indizes) aussehen, aber für andere B_IDs, die 13. Indizes möglicherweise neu erstellen?

Bearbeitet 2: Ausführungsplan

Also habe ich die Abfrage abgebrochen (die Datenbank und ihre Dateien tatsächlich gelöscht und dann wiederhergestellt) und den Ausführungsplan auf Folgendes überprüft:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13

Der (geschätzte) Ausführungsplan ist der gleiche wie für jede B.ID und sieht ziemlich einfach aus. Die WHERE-Klausel verwendet eine Indexsuche für einen nicht gruppierten Index von B, die JOIN verwendet eine gruppierte Indexsuche für beide PKs der Tabellen. Die Clustered-Index-Suche für A verwendet Parallelität (x7) und repräsentiert 90% der CPU-Zeit.

Noch wichtiger ist, dass die Abfrage mit der ID 13 tatsächlich sofort ausgeführt wird.

Bearbeitet 3: Indexfragmentierung

Die Struktur der Indizes ist wie folgt:

B hat eine gruppierte PK (nicht das ID-Feld) und einen nicht gruppierten eindeutigen Index. Das erste Feld ist B.ID - dieser zweite Index scheint immer verwendet zu werden.

A hat eine gruppierte PK (Feld nicht verwandt).

Es gibt auch 7 Ansichten zu A (alle enthalten das A.X-Feld), jede mit einer eigenen Cluster-PK, und einen anderen Index der auch das A.X-Feld enthält

Die Ansichten werden gefiltert (mit Feldern, die nicht in dieser Gleichung enthalten sind), daher bezweifle ich, dass das UPDATE A die Ansichten selbst auf irgendeine Weise verwenden würde . Sie haben jedoch einen Index, der A.X enthält. Wenn Sie also A.X ändern, müssen Sie die 7 Ansichten und die 7 Indizes schreiben, die das Feld enthalten.

Obwohl erwartet wird, dass das UPDATE dafür langsamer ist, gibt es keinen Grund, warum eine bestimmte ID so viel länger wäre als die anderen.

Ich habe die Fragmentierung für alle Indizes überprüft, alle lagen bei <0,1%, mit Ausnahme der Sekundärindizes der Ansichten, alle zwischen 25% und 50%. Füllfaktoren für alle Indizes scheinen in Ordnung zu sein, zwischen 90% und 95%.

Ich habe alle Sekundärindizes neu organisiert und mein Skript erneut ausgeführt.

Es wird immer noch gehängt, aber an einem anderen Punkt:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

Während zuvor das Nachrichtenprotokoll folgendermaßen aussah:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

        Updating A for B_ID=13

Das ist seltsam, weil es bedeutet, dass es nicht einmal an derselben Stelle in der WHILE -Schleife hängt. Der Rest sieht gleich aus: Dieselbe UPDATE-Zeile wartet in sp_who2, der gleiche PAGEIOLATCH_EX-Wartetyp und die gleiche starke HD-Nutzung von sqlserver.exe.

Der nächste Schritt besteht darin, alle Indizes und Ansichten zu löschen und neu zu erstellen, denke ich.

Bearbeitet 4: Indizes löschen und dann neu erstellen

Also habe ich alle indizierten Ansichten gelöscht, die ich in der Tabelle hatte (7 davon, 2 Indizes pro Ansicht, einschließlich der gruppierten). Ich habe das erste Skript (ohne Cursor) ausgeführt und es wurde tatsächlich in 5 Minuten ausgeführt.

Mein Problem ergibt sich also aus der Existenz dieser Indizes.

Ich habe meine Indizes nach dem Ausführen des Updates neu erstellt und es dauerte 16 Minuten.

Jetzt verstehe ich, dass die Wiederherstellung von Indizes einige Zeit in Anspruch nimmt, und die gesamte Aufgabe dauert 20 Minuten.

Was ich immer noch nicht verstehe, ist, warum es mehrere Stunden dauert, wenn ich das Update ausführe, ohne zuerst die Indizes zu löschen, aber wenn ich sie zuerst lösche und dann neu erstelle, dauert es 20 Minuten. Sollte es nicht ungefähr so ​​lange dauern?

8
GFK
  1. Halten Sie sich an den Befehl UPDATE. CURSOR ist langsamer für das, was Sie versuchen zu tun.
  2. Löschen/Deaktivieren Sie alle Indizes, einschließlich der Indizes für indizierte Ansichten. Wenn Sie einen Fremdschlüssel in A.X haben, lassen Sie ihn fallen.
  3. Erstellen Sie einen Index, der nur A.B_ID und einen weiteren für B.ID enthält.
  4. Obwohl Sie das Simple Recovery-Modell verwenden, befindet sich die letzte Transaktion immer im Transaktionsprotokoll, bevor sie auf die Festplatte geschrieben wird. Aus diesem Grund müssen Sie Ihr Transaktionsprotokoll vorab erweitern und so einstellen, dass es um einen größeren Betrag (z. B. 100 MB) erweitert wird.
  5. Stellen Sie außerdem das Wachstum von Datendateien auf einen größeren Wert ein.
  6. Stellen Sie sicher, dass Sie über genügend Speicherplatz für das weitere Wachstum von Protokoll- und Datendateien verfügen.
  7. Wenn die Aktualisierung abgeschlossen ist, erstellen/aktivieren Sie Indizes neu, die Sie in Schritt 2 gelöscht/deaktiviert haben.
  8. Wenn Sie sie nicht mehr benötigen, löschen Sie die in Schritt 3 erstellten Indizes.

Bearbeiten: Da ich Ihren ursprünglichen Beitrag nicht kommentieren kann, beantworte ich hier Ihre Frage aus Bearbeiten 4. Sie haben 7 Indizes zu A.X. Der Index ist ein B-Baum , und jede Aktualisierung dieses Felds bewirkt, dass der B-Baum neu ausgeglichen wird. Es ist schneller, diese Indizes von Grund auf neu zu erstellen, als sie jedes Mal neu auszugleichen.

0
bojan

Das Aktualisierungsszenario ist immer schneller als die Verwendung einer Prozedur.

Da Sie Spalte X aller Zeilen in Tabelle A aktualisieren, müssen Sie zuerst den Index für diese Zeile löschen. Stellen Sie außerdem sicher, dass in dieser Spalte keine Trigger und Einschränkungen aktiv sind.

Das Aktualisieren von Indizes ist ein kostspieliges Geschäft, ebenso wie das Überprüfen von Einschränkungen und das Ausführen von Triggern auf Zeilenebene, die eine Suche in anderen Daten durchführen.

0
ik_zelf

Eine Sache zu betrachten sind die Systemressourcen (Speicher, Festplatte, CPU) während dieses Prozesses. Ich habe versucht, in einem großen Auftrag 7 Millionen einzelne Zeilen in eine einzelne Tabelle einzufügen, und mein Server hing auf ähnliche Weise wie Sie.

Es stellte sich heraus, dass ich nicht genügend Speicher auf meinem Server hatte, um diesen Masseneinfügejob auszuführen. In solchen Situationen hält SQL den Speicher gerne fest und lässt ihn nicht los ... auch nachdem der Einfügebefehl möglicherweise ausgeführt wurde oder nicht. Je mehr Befehle in großen Jobs verarbeitet werden, desto mehr Speicher wird verbraucht. Durch einen schnellen Neustart wurde der Speicher freigegeben.

Ich würde diesen Prozess von Grund auf neu starten, während Ihr Task-Manager ausgeführt wird. Wenn die Speichernutzung über 75% steigt, steigt die Wahrscheinlichkeit, dass Ihr System/Ihre Prozesse einfrieren, astronomisch sprunghaft an.

Wenn Ihr Speicher/Ihre Ressourcen tatsächlich wie oben angegeben begrenzt sind, können Sie den Prozess in kleinere Teile zerlegen (mit gelegentlichem Neustart bei hoher Speichernutzung) anstelle eines großen Auftrags oder ein Upgrade auf einen 64-Bit-Server mit viel Speicher.

0
Techie Joe