SQL Server 的 8 KB 数据页中未使用 512 字节

Alp*_*mum 13 index sql-server data-pages t-sql

我创建了下表:

CREATE TABLE dbo.TestStructure
(
    id INT NOT NULL,
    filler1 CHAR(36) NOT NULL,
    filler2 CHAR(216) NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

然后创建了一个聚集索引:

CREATE CLUSTERED INDEX idx_cl_id 
ON dbo.TestStructure(id);
Run Code Online (Sandbox Code Playgroud)

接下来我用 30 行填充它,每个大小为 256 字节(基于表声明):

DECLARE @i AS int = 0;

WHILE @i < 30
BEGIN
    SET @i = @i + 1;

    INSERT INTO dbo.TestStructure (id, filler1, filler2)
    VALUES (@i, 'a', 'b');
END;
Run Code Online (Sandbox Code Playgroud)

现在根据我在“Training Kit (Exam 70-461): Querying Microsoft SQL Server 2012 (Itzik Ben-Gan)”一书中读到的信息:

SQL Server 在内部以页为单位组织数据文件中的数据。一个页面是一个 8 KB 的单位,属于一个单一的对象;例如,到表或索引。页是读取和写入的最小单位。页面被进一步组织成范围。一个区由八个连续的页面组成。一个范围内的页面可以属于单个对象或多个对象。如果页面属于多个对象,则该范围称为混合范围;如果页面属于单个对象,则该范围称为统一范围。SQL Server 将对象的前八页存储在混合区中。当对象超过八页时,SQL Server 会为此对象分配额外的统一区。通过这种组织,小物体浪费的空间更少,大物体的碎片化程度也更低。

所以这里我有第一个混合范围 8KB 页面,填充了 7680 字节(我插入了 30 次 256 字节大小的行,所以 30 * 256 = 7680),检查大小我已经运行 size check proc - 它返回以下结果

index_type_desc: CLUSTERED INDEX
index_depth: 1
index_level: 0 
page_count: 1 
record_count: 30 
avg_page_space_used_in_percent: 98.1961947121324
name : TestStructure        
rows : 30   
reserved :  16 KB
data : 8 KB 
index_size : 8 KB       
unused :    0 KB
Run Code Online (Sandbox Code Playgroud)

因此为表保留了 16 KB,第一个 8 KB 页面用于 Root IAM 页面,第二个用于叶数据存储页面,即 8KB,占用约 7.5 KB,现在当我插入一个 256 字节的新行时:

INSERT INTO dbo.TestStructure (id, filler1, filler2)
VALUES (1, 'a', 'b');
Run Code Online (Sandbox Code Playgroud)

尽管它有 256 字节的空间(7680 b + 256 = 7936 仍然小于 8KB),但它没有存储在同一页中,创建了一个新的数据页,但该新行可以放在同一个旧页上,为什么 SQL Server 会在可以节省空间和搜索时间的情况下创建一个新页面,而是将其插入到现有页面中?

注意:堆索引中也发生了同样的事情。

Sol*_*zky 11

虽然 SQL Server 确实使用 8k(8192 字节)数据页来存储 1 行或更多行,但每个数据页都有一些开销(96 字节),而每一行都有一些开销(至少 9 字节)。8192 字节不是纯粹的数据。

有关其工作原理的更详细检查,请参阅我对以下 DBA.SE 问题的回答:

与 sys.allocation_units 中的表大小不匹配的 DATALENGTH 总和

使用该链接答案中的信息,我们可以更清楚地了解实际行大小:

  1. 行头 = 4 个字节
  2. 列数 = 2 字节
  3. NULL 位图 = 1 字节
  4. 版本信息** = 14 字节(可选,见脚注)
  5. 每行总开销(不包括槽阵列)= 最少 7 个字节,如果存在版本信息,则为 21 个字节
  6. 总实际行大小 = 263 最小值(256 数据 + 7 开销),或 277 字节(256 数据 + 21 开销)(如果存在版本信息)
  7. 添加到 Slot Array 中,每行占用的总空间实际上是 265 字节(不含版本信息)或 279 字节(含版本信息)。

UsingDBCC PAGE通过显示:(Record Size 263对于tempdb)和Record Size 277(对于设置为 的数据库)来确认我的计算ALLOW_SNAPSHOT_ISOLATION ON

现在,有 30 行,即:

  • 没有版本信息

    30 * 263 会给我们 7890 字节。然后为使用的 7986 字节添加页头的 96 字节。最后,添加槽数组的 60 个字节(每行 2 个),页面上总共使用了 8046 个字节,剩余 146 个。使用DBCC PAGE通过显示来确认我的计算:

    • m_slotCnt 30 (即行数)
    • m_freeCnt 146 (即页面上剩余的字节数)
    • m_freeData 7986 (即数据 + 页头 -- 7890 + 96 -- 槽数组不计入“已用”字节计算)
  • 有版本信息

    30 * 277 字节共 8310 字节。但是 8310 超过了 8192,这甚至没有考虑 96 字节的页头,也没有考虑每行 2 字节的插槽数组(30 * 2 = 60 字节),这应该只为我们提供 8036 个可用字节的行。

    但是,29 行呢?这将为我们提供 8033 字节的数据 (29 * 277) + 96 字节的页头 + 58 字节的槽数组 (29 * 2) 等于 8187 字节。这将使页面剩余 5 个字节(8192 - 8187;当然不可用)。使用DBCC PAGE通过显示来确认我的计算:

    • m_slotCnt 29 (即行数)
    • m_freeCnt 5 (即页面上剩余的字节数)
    • m_freeData 8129 (即数据 + 页头 -- 8033 + 96 -- 槽数组不计入“已用”字节计算)

关于堆

堆填充数据页的方式略有不同。他们对页面上剩余的空间量进行了非常粗略的估计。当在DBCC输出看,看一行:PAGE HEADER: Allocation Status PFS (1:1)。你会看到VALUE沿的线条显示的东西0x60 MIXED_EXT ALLOCATED 0_PCT_FULL(我看了一下聚集表),或0x64 MIXED_EXT ALLOCATED 100_PCT_FULL在看堆表时。这是按事务评估的,因此执行单独的插入(例如此处执行的测试)可能会显示集群表和堆表之间的不同结果。但是,对所有 30 行执行单个 DML 操作将按预期填充堆。

然而,这些关于堆的细节都没有直接影响这个特定的测试,因为表的两个版本都适合 30 行,只剩下 146 个字节。无论是集群还是堆,这都不足以容纳另一行。

请记住,这个测试相当简单。计算行的实际大小可能会变得非常复杂,具体取决于各种因素,例如:SPARSE、数据压缩、LOB 数据等。


要查看数据页面的详细信息,请使用以下查询:

DECLARE @PageID INT,
        @FileID INT,
        @DatabaseID SMALLINT = DB_ID();

SELECT  @FileID = alloc.[allocated_page_file_id],
        @PageID = alloc.[allocated_page_page_id]
FROM    sys.dm_db_database_page_allocations(@DatabaseID,
                            OBJECT_ID(N'dbo.TestStructure'), 1, NULL, 'DETAILED') alloc
WHERE   alloc.[previous_page_page_id] IS NULL -- first data page
AND     alloc.[page_type] = 1; -- DATA_PAGE

DBCC PAGE(@DatabaseID, @FileID, @PageID, 3) WITH TABLERESULTS;
Run Code Online (Sandbox Code Playgroud)

**如果您的数据库设置为ALLOW_SNAPSHOT_ISOLATION ON或 ,则将出现 14 字节的“版本信息”值READ_COMMITTED_SNAPSHOT ON


小智 9

您的数据行不是 256 字节。每一个更像是 263 个字节。由于 SQL Server 中数据行的结构,纯固定长度数据类型的数据行具有额外的开销。查看此站点并了解数据行的构成方式。 http://aboutsqlserver.com/2013/10/15/sql-server-storage-engine-data-pages-and-data-rows/

因此,在您的示例中,您有一个 256 字节的数据行,为状态位添加 2 个字节,为列数添加 2 个字节,为数据长度添加 2 个字节,为空位图添加 1 个左右的字节。即 263 * 30 = 7,890 字节。添加另一个 263 并且您超过了 8kb 页面限制,这将强制创建另一个页面。