Warum verursacht ALTER COLUMN bis NOT NULL ein massives Wachstum der Protokolldateien?

56

Ich habe eine Tabelle mit 64m Reihen, die 4.3 GB auf Platte für seine Daten nimmt.

Jede Zeile besteht aus ungefähr 30 Byte Ganzzahlspalten plus einer variablen Textspalte NVARCHAR(255).

Ich habe eine NULLABLE-Spalte mit Datentyp hinzugefügt Datetimeoffset(0).

Ich habe dann diese Spalte für jede Zeile AKTUALISIERT und sichergestellt, dass alle neuen Einfügungen einen Wert in diese Spalte einfügen.

Sobald es keine NULL-Einträge gab, führte ich diesen Befehl aus, um mein neues Feld als Pflichtfeld festzulegen:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

Das Ergebnis war ein RIESIGES Wachstum der Transaktionsprotokollgröße - von 6 GB auf über 36 GB, bis der Speicherplatz knapp wurde!

Hat jemand eine Idee, was in aller Welt SQL Server 2008 R2 tut, damit dieser einfache Befehl zu einem so enormen Wachstum führt?

PapillonUK
quelle
7
SQL Server 2012 Enterprise bietet die Möglichkeit , eine NOT NULLSpalte mit einem Standard als Metadatenvorgang hinzuzufügen . Siehe auch "Hinzufügen von NOT NULL-Spalten als Online-Operation" in der Dokumentation .
Paul White

Antworten:

48

Wenn Sie eine Spalte in NOT NULL ändern, muss SQL Server jede einzelne Seite berühren , auch wenn keine NULL-Werte vorhanden sind. Abhängig von Ihrem Füllfaktor kann dies tatsächlich zu vielen Seitenteilen führen. Natürlich muss jede Seite, die berührt wird, protokolliert werden, und ich vermute aufgrund der Aufteilung, dass möglicherweise zwei Änderungen für viele Seiten protokolliert werden müssen. Da dies alles in einem einzigen Durchgang erledigt wird, muss das Protokoll alle Änderungen berücksichtigen, sodass es genau weiß, was rückgängig zu machen ist, wenn Sie auf Abbrechen klicken.


Ein Beispiel. Einfacher Tisch:

DROP TABLE dbo.floob;
GO

CREATE TABLE dbo.floob
(
  id INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
  bar INT NULL
);

INSERT dbo.floob(bar) SELECT NULL UNION ALL SELECT 4 UNION ALL SELECT NULL;

ALTER TABLE dbo.floob ADD CONSTRAINT df DEFAULT(0) FOR bar

Schauen wir uns nun die Seitendetails an. Zuerst müssen wir herausfinden, mit welcher Seite und DB_ID wir es zu tun haben. In meinem Fall habe ich eine Datenbank namens erstellt foound die DB_ID war zufällig 5.

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();

Die Ausgabe zeigte an, dass ich an Seite 159 interessiert war (die einzige Zeile in der DBCC INDAusgabe mit PageType = 1).

Schauen wir uns nun einige ausgewählte Seitendetails an, während wir das OP-Szenario durchlaufen.

DBCC PAGE(5, 1, 159, 3);

Bildbeschreibung hier eingeben

UPDATE dbo.floob SET bar = 0 WHERE bar IS NULL;    
DBCC PAGE(5, 1, 159, 3);

Bildbeschreibung hier eingeben

ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;
DBCC PAGE(5, 1, 159, 3);

Bildbeschreibung hier eingeben

Jetzt habe ich nicht alle Antworten auf diese Frage, da ich kein Kerl mit tiefen Interna bin. Es ist jedoch klar, dass sowohl die Aktualisierungsoperation als auch die Hinzufügung der NOT NULL-Einschränkung unbestreitbar auf die Seite schreiben - letztere jedoch auf eine ganz andere Weise. Es scheint tatsächlich die Struktur des Datensatzes zu ändern, anstatt nur mit Bits herumzuspielen, indem die nullfähige Spalte durch eine nicht nullfähige Spalte ersetzt wird. Warum das so ist, weiß ich nicht genau - eine gute Frage für das Storage-Engine-Team , denke ich. Ich glaube, dass SQL Server 2012 einige dieser Szenarien viel besser handhabt, FWIW - aber ich muss noch keine umfassenden Tests durchführen.

Aaron Bertrand
quelle
4
Dieses Verhalten wurde in späteren Versionen von SQL Server erheblich geändert. Ich habe 2016 RC2 überprüft und festgestellt, dass für dieses genaue Szenario und 1 Million Zeilen in der Tabelle beim Wechsel von NULL zu NOT NULL nur 29 Protokollsätze generiert werden, wenn für die Spalte bereits alle Werte angegeben wurden.
Endrju
32

Bei der Ausführung des Befehls

ALTER COLUMN ... NOT NULL

Dies scheint als Operation zum Hinzufügen von Spalten, Aktualisieren und Löschen von Spalten implementiert zu sein.

  • Eine neue Zeile wird eingefügt sys.sysrscols, um eine neue Spalte darzustellen. Das statusBit für 128ist gesetzt, um anzuzeigen, dass die Spalte kein NULLs zulässt
  • In jeder Zeile der Tabelle wird eine Aktualisierung durchgeführt, wobei der neue Spaltenwert auf den alten Spaltenwert gesetzt wird. Wenn die "Vorher" - und "Nachher" -Versionen der Zeile genau gleich sind, wird nichts in das Transaktionsprotokoll geschrieben, andernfalls wird die Aktualisierung protokolliert.
  • Die ursprüngliche Spalte wird als gelöscht markiert (dies ist nur eine Änderung der Metadaten in sys.sysrscols. Auf rscolideine große Ganzzahl aktualisiert und statusBit 2 auf "gelöscht" gesetzt).
  • Der Eintrag in sys.sysrscolsfür die neue Spalte wird geändert, um ihn rscolidder alten Spalte zuzuweisen.

Die Operation, die möglicherweise eine Menge Protokollierung verursacht, ist die UPDATEaller Zeilen in der Tabelle. Dies bedeutet jedoch nicht, dass dies immer der Fall ist . Wenn die "Vorher" - und "Nachher" -Images der Zeile identisch sind, wird dies als nicht aktualisierendes Update behandelt und bisher nicht von meinen Tests protokolliert.

Die Erklärung, warum Sie viel protokollieren, hängt also davon ab, warum genau die "Vorher" - und "Nachher" -Versionen der Zeile nicht identisch sind.

Für Spalten mit variabler Länge, die im FixedVarFormat gespeichert sind, wurde festgestellt, dass die Einstellung " NOT NULLImmer" eine Änderung in der Zeile bewirkt, die protokolliert werden muss. Die Spaltenanzahl und die Spaltenanzahl variabler Länge werden erhöht, und die neue Spalte wird am Ende des Abschnitts variabler Länge hinzugefügt, in dem die Daten dupliziert werden.

datetimeoffset(0)ist jedoch eine feste Länge, und für Spalten mit fester Länge, die in dem FixedVarFormat gespeichert sind, scheinen die alten und neuen Spalten den gleichen Schlitz in dem Datenabschnitt mit fester Länge der Zeile zu haben, und da sie beide die gleiche Länge und den gleichen Wert wie "vor" und "vor" haben "after" -Versionen der Zeile sind gleich . Dies ist in der Antwort von @ Aaron zu sehen. Beide Versionen der Zeile vor und nach dem ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;sind

0x10000c00 01000000 00000000 020000

Dies ist nicht angemeldet.

Logischerweise sollte die Zeile von meiner Beschreibung der Ereignisse hier tatsächlich anders sein, da die Spaltenanzahl 02auf erhöht werden sollte, 03aber in der Praxis tritt eine solche Änderung tatsächlich nicht auf.

Einige mögliche Gründe, warum dies in einer Spalte mit fester Länge auftreten kann, sind

  • Wenn die Spalte ursprünglich als deklariert wurde, SPARSEwird die neue Spalte in einem anderen Teil der Zeile als das Original gespeichert, was dazu führt, dass die Bilder vor und nach der Zeile unterschiedlich sind.
  • Wenn Sie eine der Komprimierungsoptionen verwenden, unterscheiden sich die Vorher- und Nachher-Versionen der Zeile, da die Spaltenanzahl im CD-Array erhöht wird.
  • Bei Datenbanken mit einer aktivierten Snapshot-Isolationsoption werden die Versionsinformationen in jeder Zeile aktualisiert (@SQL Kiwi weist darauf hin, dass dies auch in Datenbanken ohne aktivierte SI erfolgen kann, wie hier beschrieben ).
  • Möglicherweise wurde eine vorherige ALTER TABLEOperation nur als Metadatenänderung implementiert und noch nicht auf die Zeile angewendet. Wenn beispielsweise eine neue Spalte mit variabler Länge mit Nullwert hinzugefügt wurde, wird diese ursprünglich nur als Metadatenänderung angewendet und erst bei der nächsten Aktualisierung tatsächlich in die Zeilen geschrieben Abschnitt der Spaltenzahl und die NULL_BITMAPals eine NULL varcharSäule am Ende der Reihe nimmt keinen Platz)
Martin Smith
quelle
5

Ich hatte das gleiche Problem mit einer Tabelle mit 200.000.000 Zeilen. Zuerst habe ich die Spalte nullable hinzugefügt, dann alle Zeilen aktualisiert und schließlich die Spalte NOT NULLdurch eine ALTER TABLE ALTER COLUMNAnweisung in geändert . Dies führte zu zwei riesigen Transaktionen, die das Logfile in die Luft jagten (170 GB Wachstum).

Der schnellste Weg, den ich gefunden habe, war der folgende:

  1. Fügen Sie die Spalte mit einem Standardwert hinzu

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. Löschen Sie die Standardeinschränkung, indem Sie dynamisches SQL verwenden, da die Einschränkung zuvor nicht benannt wurde:

    DECLARE 
        @constraint_name SYSNAME,
        @stmt NVARCHAR(510);
    
    SELECT @CONSTRAINT_NAME = DC.NAME
    FROM SYS.DEFAULT_CONSTRAINTS DC
    INNER JOIN SYS.COLUMNS C
        ON DC.PARENT_OBJECT_ID = C.OBJECT_ID
        AND DC.PARENT_COLUMN_ID = C.COLUMN_ID
    WHERE
        PARENT_OBJECT_ID = OBJECT_ID('table1')
        AND C.NAME = 'column1';

Die Ausführungszeit wurde von> 30 Minuten auf 10 Minuten verkürzt, einschließlich der Replikation der Änderungen über die Transaktionsreplikation. Ich führe eine SQL Server 2008-Installation (SP2) aus.

Fritz
quelle
2

Ich habe folgenden Test durchgeführt:

create table tblCheckResult(
        ColID   int identity
    ,   dtoDateTime Datetimeoffset(0) null
    )

 go

insert into tblCheckResult (dtoDateTime)
select getdate()
go 10000

checkpoint 

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

select * from fn_dblog(null,null)

Ich glaube, dass dies mit dem reservierten Speicherplatz im Protokoll zu tun hat, nur für den Fall, dass Sie die Transaktion zurücksetzen. Sehen Sie sich in der Funktion fn_dblog die Spalte 'Log Reserve' für die Zeile LOP_BEGIN_XACT an und prüfen Sie, wie viel Speicherplatz reserviert werden soll.

Keith Tate
quelle
Wenn Sie versuchen select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW', können Sie die 10000 Zeilenaktualisierungen anzeigen.
Martin Smith
-2

Das Verhalten ist in SQL Server 2012 anders. Siehe http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Die Anzahl der für SQL Server 2008 R2 und Versionen darunter generierten Protokolldatensätze ist erheblich höher als die Anzahl der Protokolldatensätze für SQL Server 2012.

Fehlerbehebung in SQL
quelle
2
Die Frage ist, warum das Ändern einer vorhandenen Spalte zur NOT NULLProtokollierung führt. Bei der Änderung im Jahr 2012 geht es darum, eine neue NOT NULLSpalte mit einem Standard hinzuzufügen .
Martin Smith