将一列从 NOT NULL 更改为 NULL - 幕后发生了什么?

Ran*_*der 26 sql-server sql-server-2008-r2 alter-table database-internals

我们有一个包含 2.3B 行的表。我们想将一列从 NOT NULL 更改为 NULL。该列包含在一个索引中(不是聚集索引或 PK 索引)。数据类型没有改变(它是一个 INT)。只是可空性。声明如下:

Alter Table dbo.Workflow Alter Column LineId Int NULL
Run Code Online (Sandbox Code Playgroud)

该操作在我们停止之前需要超过 10 次(我们甚至还没有让它运行完成,因为它是一个阻塞操作并且花费的时间太长)。我们可能会将表复制到开发服务器以测试实际需要多长时间。但是,我很好奇是否有人知道 SQL Server 在从 NOT NULL 转换为 NULL 时在幕后做了什么?此外,受影响的索引是否需要重建?生成的查询计划并不表明发生了什么。

有问题的表是集群的(不是堆)。

Mar*_*ith 28

正如@Souplex 在评论中所提到的,一种可能的解释可能是,如果此列是NULL它参与的非聚集索引中的第一个可列。

对于以下设置

CREATE TABLE Foo
  (
     A UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
     B CHAR(1) NOT NULL DEFAULT 'B'
  )

CREATE NONCLUSTERED INDEX ix
  ON Foo(B);

INSERT INTO Foo
            (B)
SELECT TOP 100000 'B'
FROM   master..spt_values v1,
       master..spt_values v2 
Run Code Online (Sandbox Code Playgroud)

sys.dm_db_index_physical_stats 显示非聚集索引ix有 248 个叶页和一个根页。

索引叶页中的典型行看起来像

在此处输入图片说明

并在根页面

在此处输入图片说明

然后跑...

CHECKPOINT;

GO

ALTER TABLE Foo ALTER COLUMN B  CHAR(1) NULL;


SELECT Operation, 
       Context,
       ROUND(SUM([Log Record Length]) / 1024.0,1) AS [Log KB],
       COUNT(*) as [OperationCount]
FROM sys.fn_dblog(NULL,NULL)
WHERE AllocUnitName = 'dbo.Foo.ix'
GROUP BY Operation, Context
Run Code Online (Sandbox Code Playgroud)

回来

+-----------------+--------------------+-------------+----------------+
|    Operation    |      Context       |   Log KB    | OperationCount |
+-----------------+--------------------+-------------+----------------+
| LOP_SET_BITS    | LCX_GAM            | 4.200000    |             69 |
| LOP_FORMAT_PAGE | LCX_IAM            | 0.100000    |              1 |
| LOP_SET_BITS    | LCX_IAM            | 4.200000    |             69 |
| LOP_FORMAT_PAGE | LCX_INDEX_INTERIOR | 8.700000    |              3 |
| LOP_FORMAT_PAGE | LCX_INDEX_LEAF     | 2296.200000 |            285 |
| LOP_MODIFY_ROW  | LCX_PFS            | 16.300000   |            189 |
+-----------------+--------------------+-------------+----------------+
Run Code Online (Sandbox Code Playgroud)

再次检查索引叶,行现在看起来像

在此处输入图片说明

以及上层页面中的行如下。

在此处输入图片说明

每行都已更新,现在包含用于列计数的两个字节以及用于 NULL_BITMAP 的另一个字节。

由于额外的行宽,非聚集索引现在有 285 个叶页和两个中间级页以及根页。

的执行计划

 ALTER TABLE Foo ALTER COLUMN B  CHAR(1) NULL;
Run Code Online (Sandbox Code Playgroud)

看起来如下

在此处输入图片说明

这会创建一个全新的索引副本,而不是更新现有副本并需要拆分页面。


Spö*_*rri 9

它肯定会重新创建非聚集索引,而不仅仅是更新元数据。这是在 SQL 2014 上测试的,不应该在生产系统上测试:

CREATE TABLE [z](
    [a] [int] IDENTITY(1,1) NOT NULL,
    [b] [int] NOT NULL,
 CONSTRAINT [c_a] PRIMARY KEY CLUSTERED  ([a] ASC))
go
CREATE NONCLUSTERED INDEX [nc_b] on z (b asc)
GO
insert into z (b)
values (1);
Run Code Online (Sandbox Code Playgroud)

现在是有趣的部分:

DBCC IND (0, z, -1)
Run Code Online (Sandbox Code Playgroud)

这将为我们提供存储表和非聚集索引的数据库页面。

找到PagePIDwhere IndexIDis 2 和PageTypeis 2,然后执行以下操作:

DBCC TRACEON(3604) --are you sure that you are allowed to do this?
Run Code Online (Sandbox Code Playgroud)

进而:

dbcc page (0, 1, PagePID, 3) with tableresults
Run Code Online (Sandbox Code Playgroud)

请注意,标题中有一个空位图:

页眉提取

现在让我们做:

alter table z alter Column b int null;
Run Code Online (Sandbox Code Playgroud)

如果您实在不耐烦,您可以尝试dbcc page再次运行该命令,但它会失败,因此让我们再次使用DBCC IND (0, z, -1). 页面会像变魔术一样移动。

因此,更改列的可空性将影响覆盖该列的非聚集索引的存储,因为元数据需要更新,之后您不需要重建索引。


从 SQL Server 2016 开始ALTER TABLE ... ALTER COLUMN ...可以执行许多操作ONLINE,但是:

ALTER TABLE (Transact-SQL)

  • 改变从一列NOT NULLNULL时变更列由非聚簇索引引用不支持作为一个在线操作。