为什么 ALTER COLUMN 为 NOT NULL 会导致大量日志文件增长?

Pap*_*nUK 58 null sql-server sql-server-2008-r2 alter-table transaction-log

我有一个包含 64m 行的表,在磁盘上占用了 4.3 GB 的数据。

每行大约有 30 个字节的整数列,加上一个NVARCHAR(255)用于文本的变量列。

我添加了一个带有 data-type 的 NULLABLE 列Datetimeoffset(0)

然后我为每一行更新了这一列,并确保所有新的插入都在这一列中放置了一个值。

一旦没有 NULL 条目,我就运行这个命令来使我的新字段成为强制性的:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL
Run Code Online (Sandbox Code Playgroud)

结果是事务日志大小大幅增长——从 6GB 增加到超过 36GB,直到空间用完!

有没有人知道 SQL Server 2008 R2 到底在为这个简单的命令做些什么来导致如此巨大的增长?

Aar*_*and 49

当您将一列更改为 NOT NULL 时,即使没有 NULL 值,SQL Server 也必须访问每一页。根据您的填充因子,这实际上可能会导致大量页面拆分。当然,每个被触及的页面都必须被记录,我怀疑由于拆分,可能必须为许多页面记录两个更改。但是,由于这一切都是在一次传递中完成的,因此日志必须考虑所有更改,以便在您点击取消时,它确切地知道要撤消什么。


一个例子。简单表:

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
Run Code Online (Sandbox Code Playgroud)

现在,让我们看一下页面详细信息。首先,我们需要找出我们正在处理的页面和 DB_ID。就我而言,我创建了一个名为 的数据库foo,而 DB_ID 恰好是 5。

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();
Run Code Online (Sandbox Code Playgroud)

输出表明我对第 159 页(DBCC IND输出中唯一带有 的行PageType = 1)感兴趣。

现在,让我们在逐步完成 OP 的场景时查看一些选择页面的详细信息。

DBCC PAGE(5, 1, 159, 3);
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

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

在此处输入图片说明

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

在此处输入图片说明

现在,我没有所有的答案,因为我不是一个深入的内部人员。但很明显 - 虽然更新操作和添加 NOT NULL 约束不可否认地写入页面 - 后者以完全不同的方式这样做。通过将可空列替换为不可空列,它似乎实际上改变了记录的结构,而不仅仅是摆弄位。为什么它必须这样做,我不太确定 -我想这对存储引擎团队来说个好问题。我确实相信 SQL Server 2012 可以更好地处理其中一些场景,FWIW - 但我还没有进行任何详尽的测试。

  • 此行为在 SQL Server 的更高版本中发生了很大变化。我检查了 2016 RC2 并发现对于这个确切的场景和表中的 100 万行,如果已经为列指定了所有值,则在从 NULL 更改为 NOT NULL 期间仅生成 29 条日志记录。 (5认同)

Mar*_*ith 33

执行命令时

ALTER COLUMN ... NOT NULL
Run Code Online (Sandbox Code Playgroud)

这似乎是作为添加列、更新、删除列操作来实现的。

  • 插入新行sys.sysrscols以表示新列。在status对位128被设置指示列不允许NULL小号
  • 对表的每一行执行更新,将新列的值设置为旧列值的值。如果行的“之前”和“之后”版本完全相同,这不会导致任何内容写入事务日志,否则会记录更新。
  • 原始列被标记为已删除(这是仅元数据更改sys.sysrscols.rscolid更新为一个大整数,status位 2 设置为指示已删除)
  • sys.sysrscols新列的条目被更改为rscolid旧列的。

可能导致大量日志记录的操作是UPDATE表中所有行的操作,但这并不意味着这将始终发生。如果该行的“之前”和“之后”图像相同,那么这将被视为非更新更新,并且目前不会从我的测试中记录下来。

因此,关于为什么要获得大量日志记录的解释将取决于为什么该行的“之前”和“之后”版本完全不同。

对于以FixedVar格式存储的可变长度列,我发现设置为NOT NULL总是会导致需要记录的行发生变化。列计数和可变长度列计数都会增加,并且新列被添加到复制数据的可变长度部分的末尾。

datetimeoffset(0)然而,对于以这种FixedVar格式存储的固定长度列,旧列和新列似乎都在行的固定长度数据部分中被赋予了相同的槽,并且因为它们都具有相同的长度和值“之前”和行的“之后”版本是相同的。这可以在@Aaron 的回答中看到。之前和之后的行的两个版本ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;都是

0x10000c00 01000000 00000000 020000
Run Code Online (Sandbox Code Playgroud)

这没有记录。

从我对事件的描述逻辑上看,行实际上在这里应该有所不同,因为列数02应该增加到03但实际上并没有发生这种变化。

关于为什么会在固定长度列中发生这种情况的一些可能原因是

  • 如果该列最初声明为,SPARSE那么新列将存储在与原始行不同的部分,导致前后行图像不同。
  • 如果您使用任何压缩选项,那么随着 CD 数组中的列计数部分增加,行的前后版本将有所不同。
  • 在启用了其中一个快照隔离选项的数据库上,每行中的版本信息都会更新(@SQL Kiwi 指出,这也可能发生在没有启用 SI 的数据库中,如此处所述)。
  • 可能有一些先前的ALTER TABLE操作被实现为仅元数据更改并且尚未应用于该行。例如,如果添加了一个新的可为空的可变长度列,那么这最初仅作为元数据更改应用,并且仅在下次更新时实际写入行(在最后一个实例中实际发生的写入只是更新到列数部分和NULL_BITMAP作为NULL varchar行末尾的列不占用任何空间)


小智 7

对于一个有 200.000.000 行的表,我遇到了同样的问题。最初我添加了可空列,然后更新了所有行,最后将列更改为NOT NULL通过ALTER TABLE ALTER COLUMN语句。这导致两个巨大的事务令人难以置信地炸毁了日志文件(170 GB 增长)。

我找到的最快的方法如下:

  1. 使用默认值添加列

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用动态 SQL 删除默认约束,因为之前未命名约束:

    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';
    
    Run Code Online (Sandbox Code Playgroud)

执行时间从 > 30 分钟减少到 10 分钟,包括通过事务复制复制更改。我正在运行 SQL Server 2008 安装 (SP2)。