在某些情况下,删除列可以是仅元数据操作。任何给定表的列定义都不包含在存储行的每一页中,列定义仅存储在数据库元数据中,包括sys.sysrowsets,sys.sysrscols等。
当删除未由任何其他对象引用的列时,存储引擎会通过从各种系统表中删除相关详细信息来简单地将列定义标记为不再存在。删除元数据的操作会使过程高速缓存无效,从而每当查询随后引用该表时都需要重新编译。由于重新编译仅返回表中当前存在的列,因此甚至不会要求删除的列的列详细信息。存储引擎会跳过该列的每个页面中存储的字节,就好像该列不再存在。
当对该表执行后续DML操作时,受影响的页面将被重写,而没有要删除的列的数据。如果您重建聚集索引或堆,则丢弃的列的所有字节自然不会写回到磁盘上的页面。随着时间的推移,这有效地分散了下降色谱柱的负担,使其不那么引人注目。
在某些情况下,例如当该列包含在索引中或为该列手动创建统计对象时,就无法删除该列。我写了一篇博客文章,显示了尝试使用手动创建的统计对象更改列时出现的错误。删除列时使用相同的语义-如果该列被任何其他对象引用,则不能简单地将其删除。必须先更改引用对象,然后才能删除该列。
通过删除一列后查看事务日志的内容,这很容易显示。下面的代码创建一个带有8,000个长字符列的表。它添加一行,然后将其删除,并显示适用于删除操作的事务日志的内容。日志记录显示对存储表和列定义的各种系统表的修改。如果实际上是从分配给该表的页面中删除了列数据,则将看到记录实际页面数据的日志记录;没有这样的记录。
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)
(输出太大,无法在此处显示,并且dbfiddle.uk不允许我访问fn_dblog)
第一组输出显示由于DDL语句删除列而导致的日志。第二组输出显示运行DML语句后的日志,并在其中更新rid
列。在第二个结果集中,我们看到指示删除dbo.DropColumnTest,然后插入dbo.DropColumnTest的日志记录。每个日志记录长度为8116,表示实际页面已更新。
从fn_dblog
上面的测试中的命令输出中可以看到,整个操作已完全记录。这适用于简单恢复以及完全恢复。术语“完全记录”可能会误解为未记录数据修改。不会发生这种情况-修改已记录下来,可以完全回滚。日志仅记录了被触摸的页面,并且由于DDL操作未记录任何表的数据页面DROP COLUMN
,因此无论表的大小如何,以及可能发生的任何回滚都将非常迅速地发生。
为了科学起见,以下代码将使用DBCC PAGE
样式“ 3” 转储上面代码中包含的表的数据页。样式“ 3”表示我们希望页面标题加上详细的每行解释。该代码使用游标显示表中每个页面的详细信息,因此您可能需要确保不要在大型表上运行它。
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);
查看演示中第一页的输出(在删除列之后,但在更新列之前),我看到了以下内容:
页面:(1:100104)
缓冲:
BUF @ 0x0000021793E42040
bpage = 0x000002175A7A0000 bhash = 0x0000000000000000 bpageno =(1:100104)
bdbid = 10 breferences = 1 bcputicks = 0
bsampleCount = 0 bUse1 = 13760 bstat = 0x10b
博客= 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640
bstat2 = 0x0
页面标题:
页面@ 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.idIndd)= 256
元数据:AllocUnitId = 72057594057588736
元数据:PartitionId = 72057594051756032元数据:IndexId = 1
元数据: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
分配状态
GAM(1:2)=已分配SGAM(1:3)=未分配
PFS(1:97056)= 0x40已分配0_PCT_FULL DIFF(1:6)=已更改
ML(1:7)= NOT MIN_LOGGED
插槽0偏移0x60长度8015
记录类型= PRIMARY_RECORD记录属性= NULL_BITMAP VARIABLE_COLUMNS
记录大小= 8015
内存转储@ 0x000000B75227A060
0000000000000000:30000800 01000000 02000001 004f1f5a 5a5a5a5a 0 ............ O.ZZZZZZ
0000000000000014:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ
。
。
。
0000000000001F2C:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F40:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a ZZZZZZZZZZZZZZZZZ
插槽0列1偏移0x4长度4长度(物理)4
摆脱= 1
插槽0列67108865偏移量0xf长度0长度(物理)8000
删除=空
插槽0偏移量0x0长度0长度(物理)0
KeyHashValue =(8194443284a0)
为了简洁起见,我从上面显示的输出中删除了大多数原始页面转储。在输出的结尾,您将在rid
列中看到以下内容:
插槽0列1偏移0x4长度4长度(物理)4
摆脱= 1
上方的最后一行rid = 1
返回该列的名称以及存储在页面上该列中的当前值。
接下来,您将看到:
插槽0列67108865偏移量0xf长度0长度(物理)8000
删除=空
输出显示,由于DELETED
文本通常位于列名中,因此插槽0包含已删除的列。NULL
由于该列已被删除,因此返回该列的值。但是,如您在原始数据中所见REPLICATE('Z', 8000)
,该列的8,000个字符长的值仍然存在于页面上。这是DBCC PAGE输出的那部分的样本:
0000000000001EDC:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ
0000000000001EF0:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F04:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ
0000000000001F18:5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZZZ