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


56

我有一个表,其中有64m行,其数据占用磁盘上4.3 GB的空间。

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

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

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

一旦没有NULL条目,我就运行以下命令使我的新字段成为必填项:

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

结果是事务日志大小从6GB大幅增加到36GB以上,直到空间用完为止!

有谁知道SQL Server 2008 R2在做什么,以使这个简单的命令取得如此巨大的增长?


7
SQL Server 2012 Enterprise 增加了添加NOT NULL默认列作为元数据操作的功能。另请参阅文档中的“将NOT NULL列添加为在线操作” 。
保罗·怀特

Answers:


48

当您将列更改为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

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

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

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

现在,让我们看一下OP场景中的一些选定页面详细信息。

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

在此处输入图片说明

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

在此处输入图片说明

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

在此处输入图片说明

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


4
在更高版本的SQL Server中,此行为已发生很大变化。我检查了2016 RC2,发现对于这种确切的情况,如果已为该列指定了所有值,则在从NULL更改为NOT NULL的过程中,表中的100万行仅生成29条日志记录。
Endrju '16

32

执行命令时

ALTER COLUMN ... NOT NULL

这似乎是作为“添加列,更新,删除列”操作实现的。

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

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

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

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

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

0x10000c00 01000000 00000000 020000

这没有记录。

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

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

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

5

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

我发现最快的方法如下:

  1. 使用默认值添加列

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  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';

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


2

我运行了以下测试:

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)

我认为,这与日志保留的保留空间有关,以防万一您回滚事务。在LOP_BEGIN_XACT行的“日志保留”列的fn_dblog函数中查找,并查看它试图保留多少空间。


如果尝试select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW',可以看到10000行更新。
马丁·史密斯

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.