页眉中还存储了哪些其他信息

Joh*_* N. 5 sql-server sql-server-2008-r2 sql-server-2012 sql-server-2014 sql-server-2017

SQL Server 数据库页的大小定义为 8192 字节。有一些头信息据说大小为 96 字节。

如果您曾经尝试创建一个包含超过 8053 个字节的列定义的表,那么您将看到错误消息:

Creating or altering table 'Generated_Data_GUID' failed because the 
minimum row size would be 8061, including 7 bytes of internal overhead. 
This exceeds the maximum allowable table row size of 8060 bytes.
Run Code Online (Sandbox Code Playgroud)

以下是一个示例表 DDL:

CREATE TABLE [dbo].[Generated_Data_GUID](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [uniqueidentifier] NOT NULL,
    [SEQGUID] [uniqueidentifier] NOT NULL,
    [Data1] [char](4000) NULL,
    [Data2] [char](4000) NULL,
    [Data3] [char](9) NULL,
    [EntryDate] [datetime2](7) NULL
) ON [PRIMARY]
Run Code Online (Sandbox Code Playgroud)

通过以上的DDL如果我改变了列的列定义Data3char(10),那么我会打的错误消息。

每个列类型的字节大小如下:

int               :  4 bytes
uniqueidentifiere : 16 bytes
char(n)           :  n bytes
datetime2(n)      :  6 bytes if n < 3 
                     7 bytes if n = 3 or n = 4
                     8 bytes if n > 4
Run Code Online (Sandbox Code Playgroud)

如果我们做一些简单的数学运算,那么我们最终会得到以下计算:

Page Size         : 8192 bytes
                   -----------
Header            :   96 bytes - 
Internal Overhead :    7 bytes - 
Max Size          : 8053 bytes - 
                   -----------
Missing Data      :   36 bytes
                   ===========
Run Code Online (Sandbox Code Playgroud)

这 36 个字节包含什么?

参考资料

Jos*_*ell 10

Paul Randal 实际上在您链接到的博客文章的评论中回答了这个确切的问题:

8060 字节是一条记录的最大大小,而不是页面上的数据空间量 – 8096 字节。

对于 8060 字节的最大记录,为槽数组条目添加两个字节,为可能的堆转发记录反向指针添加 10 个字节,为可能的版本控制标记添加 14 个字节,即使用了 26 个字节。其他 10 个字节供将来可能使用。

如果页面上有多条记录,则可以使用所有 8096 字节的数据空间。

因此,在回答您帖子正文中的问题时:

这 36 个字节包含什么?

页中“额外”的 36 个字节的用法如下:

  • 为堆前向记录反向指针保留 10 个字节
  • 为指向 tempdb 中版本存储的版本控制标记保留的 14 个字节
  • 插槽数组有 12 个字节可用
    • 如果您概述了一个大记录,则此处有 10 个字节的“浪费”空间。还有 5 个 2 字节槽数组条目的空间

只是为了确认问题中定义的表实际上是 8060 字节宽,让我们进行完整的重现。

首先,我们将设置数据库和表,并在其中插入一行。我正在添加一个聚集索引,因为堆是最糟糕的。

USE master;
GO

CREATE DATABASE PageJunk;
GO

USE PageJunk;
GO

CREATE TABLE [dbo].[Generated_Data_GUID](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [uniqueidentifier] NOT NULL,
    [SEQGUID] [uniqueidentifier] NOT NULL,
    [Data1] [char](4000) NULL,
    [Data2] [char](4000) NULL,
    [Data3] [char](9) NULL,
    [EntryDate] [datetime2](7) NULL
) ON [PRIMARY];
GO

CREATE CLUSTERED INDEX PK_Generated_Data_GUID 
    ON Generated_Data_GUID (ID);
GO

INSERT INTO [dbo].[Generated_Data_GUID]
    ([GUID], SEQGUID, Data1, Data2, Data3, EntryDate)
VALUES
    (NEWID(), NEWID(), REPLICATE('1', 4000), REPLICATE('2', 4000), REPLICATE('3', 9), '2018-01-01');
GO
Run Code Online (Sandbox Code Playgroud)

通过运行这个 DBCC 命令,我们可以看到所有分配了索引的页面:

DBCC IND ('PageJunk', 'Generated_Data_GUID', 1);
GO
Run Code Online (Sandbox Code Playgroud)

神秘的 dbcc 废话

页类型为 1 的是索引页(页 ID 336)。我们可以使用其他 DBCC 命令转储有关该页面的各种信息:

DBCC TRACEON (3604); -- needed for the next one to work
GO

DBCC PAGE (PageJunk, 1, 336, 3);
GO
Run Code Online (Sandbox Code Playgroud)

以下是该命令输出的一些重要片段。从 HEADER 部分:

m_freeCnt = 34
Run Code Online (Sandbox Code Playgroud)

这意味着页面上有 34 字节的可用空间。这是您在原始帖子中概述的 36,减去插槽数组条目的 2 个字节。说到这:

m_slotCnt = 1
Run Code Online (Sandbox Code Playgroud)

这意味着此页面上只有一条记录。

现在,在记录部分:

Slot 0 Offset 0x60 Length 8060

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP    Record Size = 8060
Run Code Online (Sandbox Code Playgroud)

这表明该页上存储的单个记录为 8060 字节(这是所有数据类型存储大小的总和加上每条记录 7 个字节的开销)。

所以我们在这个页面上确实有一个完整大小的 8060 字节记录。但是,如果我们再努力一点,我们仍然可以在此页面上再压缩 34 个字节。

例如,我可以创建一个 2015 字节宽的表。然后每行将占用页面中的 2015 + 7(内部开销)+ 2(槽数组)= 2024 个字节。所以四行加起来应该是 8096 字节,正好填补了 96 字节标头之后剩下的空间。让我们在同一个数据库中试试:

CREATE TABLE [dbo].[QuarterPage](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [GUID] [uniqueidentifier] NOT NULL,
    [SEQGUID] [uniqueidentifier] NOT NULL,
    [Data1] [char](981) NULL,
    [Data2] [char](981) NULL,
    [Data3] [char](9) NULL,
    [EntryDate] [datetime2](7) NULL
) ON [PRIMARY];
GO

CREATE CLUSTERED INDEX PK_QuarterPage
    ON QuarterPage (ID);
GO

INSERT INTO [dbo].[QuarterPage]
    ([GUID], SEQGUID, Data1, Data2, Data3, EntryDate)
VALUES
    (NEWID(), NEWID(), REPLICATE('1', 981), REPLICATE('2', 981), REPLICATE('3', 9), '2018-01-01');
GO 4
Run Code Online (Sandbox Code Playgroud)

现在我们找到了我们的页面,只有一个符合预期的:

DBCC IND ('PageJunk', 'QuarterPage', 1);
GO
Run Code Online (Sandbox Code Playgroud)

更多 dbcc 废话

所以现在我们想要获取第 352 页的信息:

DBCC PAGE (PageJunk, 1, 352, 3);
GO
Run Code Online (Sandbox Code Playgroud)

这是好东西:

m_slotCnt = 4
m_freeCnt = 0
Run Code Online (Sandbox Code Playgroud)

没有空闲空间!这个页面被我们的 4 行打得满满当当。