Ist ALTER TABLE… DROP COLUMN wirklich nur eine Metadatenoperation?

11

Ich habe mehrere Quellen gefunden, die ALTER TABLE angeben ... DROP COLUMN ist eine reine Metadatenoperation.

Quelle

Wie kann das sein? Müssen die Daten während einer DROP COLUMN nicht aus den zugrunde liegenden nicht gruppierten Indizes und dem gruppierten Index / Heap gelöscht werden?

Warum implizieren die Microsoft-Dokumente außerdem , dass es sich um einen vollständig protokollierten Vorgang handelt?

Die an der Tabelle vorgenommenen Änderungen werden protokolliert und können vollständig wiederhergestellt werden. Änderungen, die sich auf alle Zeilen in großen Tabellen auswirken, z. B. das Löschen einer Spalte oder in einigen Editionen von SQL Server das Hinzufügen einer NOT NULL-Spalte mit einem Standardwert, können lange dauern, bis viele Protokolldatensätze abgeschlossen und generiert sind . Führen Sie diese ALTER TABLE-Anweisungen mit der gleichen Sorgfalt aus wie alle INSERT-, UPDATE- oder DELETE-Anweisungen, die viele Zeilen betreffen.

Als sekundäre Frage: Wie verfolgt die Engine abgelegte Spalten, wenn die Daten nicht von den zugrunde liegenden Seiten entfernt werden?

George.Palacios
quelle
2
Nun, ich denke, dass die Sprache durch viele Versionen des Produkts und viele weitere Iterationen der Dokumentation überlebt hat. Im Laufe der Zeit haben sich immer mehr Operationen mit Spalten nur zu Online- / Metadatenänderungen entwickelt. Es ist vielleicht ein schlechtes konkretes Beispiel jetzt, aber der Zweck des Satzes ist einfach , Sie zu warnen , dass im Allgemeinen, einige alten Operationen könnten Größe-of-Datenoperationen in seinem bestimmten Szenarien, anstatt Liste aus jedem einzelnen bestimmten Szenario.
Aaron Bertrand

Antworten:

14

Unter bestimmten Umständen kann das Löschen einer Spalte eine reine Metadatenoperation sein. Die Spaltendefinitionen für eine bestimmte Tabelle sind nicht auf jeder Seite enthalten, auf der Zeilen gespeichert werden. Spaltendefinitionen werden nur in den Datenbankmetadaten gespeichert, einschließlich sys.sysrowsets, sys.sysrscols usw.

Wenn Sie eine Spalte löschen, auf die von keinem anderen Objekt verwiesen wird, markiert die Speicher-Engine die Spaltendefinition einfach als nicht mehr vorhanden, indem sie die relevanten Details aus verschiedenen Systemtabellen löscht. Das Löschen der Metadaten macht den Prozedurcache ungültig und erfordert eine Neukompilierung, wenn eine Abfrage anschließend auf diese Tabelle verweist. Da die Neukompilierung nur Spalten zurückgibt, die derzeit in der Tabelle vorhanden sind, werden die Spaltendetails für die abgelegte Spalte nicht einmal abgefragt. Die Speicher-Engine überspringt die auf jeder Seite für diese Spalte gespeicherten Bytes, als ob die Spalte nicht mehr vorhanden wäre.

Wenn eine nachfolgende DML-Operation für die Tabelle ausgeführt wird, werden die betroffenen Seiten ohne die Daten für die abgelegte Spalte neu geschrieben. Wenn Sie einen Clustered-Index oder einen Heap neu erstellen, werden natürlich nicht alle Bytes für die abgelegte Spalte auf die Seite auf der Festplatte zurückgeschrieben. Dadurch wird die Last des Fallens der Säule im Laufe der Zeit effektiv verteilt, wodurch sie weniger auffällt.

Unter bestimmten Umständen können Sie eine Spalte nicht löschen, z. B. wenn die Spalte in einem Index enthalten ist oder wenn Sie manuell ein Statistikobjekt für die Spalte erstellt haben. Ich habe einen Blog-Beitrag geschrieben , der den Fehler zeigt, der beim Versuch auftritt, eine Spalte mit einem manuell erstellten Statistikobjekt zu ändern. Die gleiche Semantik gilt beim Löschen einer Spalte. Wenn die Spalte von einem anderen Objekt referenziert wird , kann sie nicht einfach gelöscht werden. Das referenzierende Objekt muss zuerst geändert werden, dann kann die Spalte gelöscht werden.

Dies lässt sich relativ einfach anzeigen, indem Sie den Inhalt des Transaktionsprotokolls nach dem Löschen einer Spalte anzeigen. Der folgende Code erstellt eine Tabelle mit einer einzelnen 8.000 langen Zeichenspalte. Es fügt eine Zeile hinzu, löscht sie dann und zeigt den Inhalt des Transaktionsprotokolls an, der für die Löschoperation gilt. Die Protokollsätze zeigen Änderungen an verschiedenen Systemtabellen, in denen die Tabellen- und Spaltendefinitionen gespeichert sind. Wenn die Spaltendaten tatsächlich von den der Tabelle zugewiesenen Seiten gelöscht wurden, werden Protokolldatensätze angezeigt, in denen die tatsächlichen Seitendaten aufgezeichnet werden. Es gibt keine solchen Aufzeichnungen.

DROP TABLE IF EXISTS dbo.DropColumnTest;
GO
CREATE TABLE dbo.DropColumnTest
(
    rid int NOT NULL
        CONSTRAINT DropColumnTest_pkc
        PRIMARY KEY CLUSTERED
    , someCol varchar(8000) NOT NULL
);

INSERT INTO dbo.DropColumnTest (rid, someCol)
SELECT 1, REPLICATE('Z', 8000);
GO

DECLARE @startLSN nvarchar(25);

SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

DECLARE @a int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1)
      , @b int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1)
      , @c int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

ALTER TABLE dbo.DropColumnTest DROP COLUMN someCol;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)


--modify an existing data row 
SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

SET @a = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1);
SET @b = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1);
SET @c = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

UPDATE dbo.DropColumnTest SET rid = 2;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)

(Die Ausgabe ist zu groß, um hier angezeigt zu werden, und dbfiddle.uk erlaubt mir nicht, auf fn_dblog zuzugreifen.)

Der erste Ausgabesatz zeigt das Protokoll als Ergebnis der DDL-Anweisung, die die Spalte löscht. Der zweite Ausgabesatz zeigt das Protokoll nach dem Ausführen der DML-Anweisung, in der die ridSpalte aktualisiert wird. In der zweiten Ergebnismenge sehen wir Protokolldatensätze, die ein Löschen für dbo.DropColumnTest anzeigen, gefolgt von einer Einfügung in dbo.DropColumnTest. Jede Protokolldatensatzlänge beträgt 8116, was angibt, dass die tatsächliche Seite aktualisiert wurde.

Wie Sie aus dem Ausgang des sehen fn_dblogBefehls im Test oben, der gesamte Vorgang wird vollständig protokolliert. Dies gilt sowohl für die einfache als auch für die vollständige Wiederherstellung. Die Terminologie "vollständig protokolliert" wird möglicherweise falsch interpretiert, da die Datenänderung nicht protokolliert wird. Dies ist nicht der Fall - die Änderung wird protokolliert und kann vollständig zurückgesetzt werden. Das Protokoll zeichnet einfach nur die Seiten auf, die berührt wurden, und da keine der Datenseiten der Tabelle durch die DDL-Operation protokolliert wurde, werden sowohl das DROP COLUMNals auch eventuell auftretende Rollback unabhängig von der Größe der Tabelle extrem schnell durchgeführt.

Für die Wissenschaft werden mit dem folgenden Code die Datenseiten für die im obigen Code enthaltene Tabelle im DBCC PAGEStil "3" ausgegeben. Der Stil "3" gibt an, dass wir den Seitenkopf sowie eine detaillierte Interpretation pro Zeile wünschen . Der Code verwendet einen Cursor, um die Details für jede Seite in der Tabelle anzuzeigen. Sie sollten daher sicherstellen, dass Sie dies nicht für eine große Tabelle ausführen.

DBCC TRACEON(3604); --directs out from DBCC commands to the console, instead of the error log
DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT dpa.allocated_page_file_id
    , dpa.allocated_page_page_id
FROM sys.schemas s  
    INNER JOIN sys.objects o ON o.schema_id = s.schema_id
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), o.object_id, NULL, NULL, 'DETAILED') dpa
WHERE o.name = N'DropColumnTest'
    AND s.name = N'dbo'
    AND dpa.page_type_desc = N'DATA_PAGE';
OPEN cur;
FETCH NEXT FROM cur INTO @fileid, @pageid;
WHILE @@FETCH_STATUS = 0
BEGIN
    DBCC PAGE (@dbid, @fileid, @pageid, 3);
    FETCH NEXT FROM cur INTO @fileid, @pageid;
END
CLOSE cur;
DEALLOCATE cur;
DBCC TRACEOFF(3604);

Wenn ich mir die Ausgabe für die erste Seite meiner Demo ansehe (nachdem die Spalte gelöscht wurde, aber bevor die Spalte aktualisiert wurde), sehe ich Folgendes:

SEITE: (1: 100104)


PUFFER:


BUF @ 0x0000021793E42040

bpage = 0x000002175A7A0000 bhash = 0x0000000000000000 bpageno = (1: 100104)
bdbid = 10 breferences = 1 bcputicks = 0
bsampleCount = 0 bUse1 = 13760 bstat = 0x10b
blog = 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640
bstat2 = 0x0                        

KOPFZEILE:


Seite @ 0x000002175A7A0000

m_pageId = (1: 100104) m_headerVersion = 1 m_type = 1
m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000
m_objId (AllocUnitId.idObj) = 300 m_indexId (AllocUnitId.idInd) = 256 
Metadaten: AllocUnitId = 72057594057588736                                
Metadaten: PartitionId = 72057594051756032 Metadaten: IndexId = 1
Metadaten: ObjectId = 174623665 m_prevPage = (0: 0) m_nextPage = (0: 0)
pminlen = 8 m_slotCnt = 1 m_freeCnt = 79
m_freeData = 8111 m_reservedCnt = 0 m_lsn = (616: 14191: 25)
m_xactReserved = 0 m_xdesId = (0: 0) m_ghostRecCnt = 0
m_tornBits = 0 DB Frag ID = 1                      

Zuordnungsstatus

GAM (1: 2) = ALLOCATED SGAM (1: 3) = NOT ALLOCATED          
PFS (1: 97056) = 0x40 ALLOCATED 0_PCT_FULL DIFF (1: 6) = CHANGED
ML (1: 7) = NICHT MIN_LOGGED           

Steckplatz 0 Versatz 0x60 Länge 8015

Datensatztyp = PRIMARY_RECORD Datensatzattribute = NULL_BITMAP VARIABLE_COLUMNS
Datensatzgröße = 8015                  
Speicherauszug @ 0x000000B75227A060

0000000000000000: 30000800 01000000 02000001 004f1f5a 5a5a5a5a 0 ............ O.ZZZZZ
0000000000000014: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ
.
.
.
0000000000001F2C: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
0000000000001F40: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a ZZZZZZZZZZZZZZ

Steckplatz 0 Spalte 1 Versatz 0x4 Länge 4 Länge (physisch) 4

rid = 1                             

Steckplatz 0 Spalte 67108865 Versatz 0xf Länge 0 Länge (physisch) 8000

DROPPED = NULL                      

Steckplatz 0 Versatz 0x0 Länge 0 Länge (physisch) 0

KeyHashValue = (8194443284a0)       

Der Kürze halber habe ich den größten Teil des Rohseiten-Dumps aus der oben gezeigten Ausgabe entfernt. Am Ende der Ausgabe sehen Sie dies für die ridSpalte:

Steckplatz 0 Spalte 1 Versatz 0x4 Länge 4 Länge (physisch) 4

rid = 1                             

Die letzte Zeile oben rid = 1gibt den Namen der Spalte und den aktuellen Wert zurück, der in der Spalte auf der Seite gespeichert ist.

Als nächstes sehen Sie Folgendes:

Steckplatz 0 Spalte 67108865 Versatz 0xf Länge 0 Länge (physisch) 8000

DROPPED = NULL                      

Die Ausgabe zeigt, dass Slot 0 aufgrund des DELETEDTextes, in dem sich normalerweise der Spaltenname befindet, eine gelöschte Spalte enthält . Der Wert der Spalte wird zurückgegeben, NULLseit die Spalte gelöscht wurde. Wie Sie jedoch in den Rohdaten sehen können, ist der Wert von 8.000 Zeichen REPLICATE('Z', 8000)für diese Spalte auf der Seite noch vorhanden. Dies ist ein Beispiel für diesen Teil der DBCC-PAGE-Ausgabe:

0000000000001EDC: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZ
0000000000001EF0: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ
0000000000001F04: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
0000000000001F18: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZ
Max Vernon
quelle