我发现几个来源表明 ALTER TABLE ... DROP COLUMN 是仅元数据操作。
怎么会这样?DROP COLUMN 期间的数据是否不需要从底层非聚集索引和聚集索引/堆中清除?
此外,为什么Microsoft Docs暗示它是完全记录的操作?
对表所做的修改会被记录并完全恢复。影响大型表中所有行的更改(例如删除列或在某些版本的 SQL Server 上添加具有默认值的 NOT NULL 列)可能需要很长时间才能完成并生成许多日志记录。像影响许多行的任何 INSERT、UPDATE 或 DELETE 语句一样小心运行这些 ALTER TABLE 语句。
作为第二个问题:如果数据没有从底层页面中删除,引擎如何跟踪删除的列?
在某些情况下,删除列可能是元数据操作。任何给定表的列定义不包含在存储行的每个页面中,列定义仅存储在数据库元数据中,包括 sys.sysrowsets、sys.sysrscols 等。
当删除未被任何其他对象引用的列时,存储引擎通过从各种系统表中删除相关详细信息来简单地将列定义标记为不再存在。删除元数据的操作使过程缓存无效,每当查询随后引用该表时都需要重新编译。由于重新编译只返回表中当前存在的列,因此甚至从不要求删除列的列详细信息;存储引擎跳过该列在每一页中存储的字节,就好像该列不再存在一样。
当对表进行后续 DML 操作时,受影响的页面将被重写,而删除的列的数据不包含在内。如果重建聚集索引或堆,则删除列的所有字节自然不会写回磁盘上的页面。随着时间的推移,这有效地分散了删除列的负载,使其不那么明显。
在某些情况下,您无法删除列,例如当列包含在索引中时,或者当您手动为列创建统计对象时。我写了一篇博客文章,显示了尝试使用手动创建的统计对象更改列时出现的错误。删除列时适用相同的语义 - 如果该列被任何其他对象引用,则不能简单地删除它。必须先更改引用对象,然后才能删除该列。
这很容易通过在删除一列后查看事务日志的内容来显示。下面的代码创建了一个包含单个 8,000 长字符列的表。它添加一行,然后删除它,并显示适用于删除操作的事务日志的内容。日志记录显示对存储表和列定义的各种系统表的修改。如果列数据实际上是从分配给表的页面中删除的,您会看到记录实际页面数据的日志记录;没有这样的记录。
(输出太大,无法在此处显示,dbfiddle.uk 不允许我访问 fn_dblog)
第一组输出显示日志是 DDL 语句删除列的结果。第二组输出显示运行我们更新
rid
列的 DML 语句后的日志。在第二个结果集中,我们看到指示对 dbo.DropColumnTest 进行删除的日志记录,然后是对 dbo.DropColumnTest 的插入。每个日志记录长度为 8116,表示实际页面已更新。从上面测试中的命令输出可以看出
fn_dblog
,整个操作都被完全记录了下来。这适用于简单恢复以及完全恢复。术语“完全记录”可能会被误解为未记录数据修改。这不是发生的情况 - 修改被记录,并且可以完全回滚。日志只是简单地记录被触摸的页面,并且由于 DDL 操作没有记录表的数据页DROP COLUMN
,因此无论表的大小如何,可能发生的任何回滚都将非常迅速地发生。为了科学,下面的代码将转储上面代码中包含的表格的数据页,使用
DBCC PAGE
样式“3”。样式“3”表示我们想要页眉加上详细的每行解释。该代码使用光标显示表中每个页面的详细信息,因此您可能需要确保不要在大表上运行它。查看演示中第一页的输出(在删除列之后,但在更新列之前),我看到了:
为简洁起见,我已经从上面显示的输出中删除了大部分原始页面转储。在输出的末尾,您将在
rid
列中看到:上面的最后一行
rid = 1
,返回列的名称,以及存储在页面上的列中的当前值。接下来,您将看到:
输出显示 Slot 0 包含一个已删除的列,这取决于
DELETED
列名通常所在的文本。NULL
由于该列已被删除,因此返回该列的值。但是,正如您在原始数据中看到的那样,REPLICATE('Z', 8000)
该列的 8,000 个字符长的值 , 仍然存在于页面上。这是 DBCC PAGE 输出的那部分示例: