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


13

我已经创建了下表:

CREATE TABLE dbo.TestStructure
(
    id INT NOT NULL,
    filler1 CHAR(36) NOT NULL,
    filler2 CHAR(216) NOT NULL
);

然后创建一个聚集索引:

CREATE CLUSTERED INDEX idx_cl_id 
ON dbo.TestStructure(id);

接下来,我为它填充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;

现在,根据我在“培训工具包(考试70-461):查询Microsoft SQL Server 2012(Itzik Ben-Gan)”书中阅读的信息:

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

因此,这里有第一个混合扩展区8KB页面,其中填充了7680个字节(我已插入30次256字节大小的行,所以30 * 256 = 7680),以检查我运行大小检查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

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

INSERT INTO dbo.TestStructure (id, filler1, filler2)
VALUES (1, 'a', 'b');

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

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

Answers:


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页面的限制,这将强制创建另一个页面。


您提供的链接帮助我更好地可视化了页面结构,我正在搜索类似的内容,但找不到它,Thax
Alphas Supremum

11

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

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

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

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

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

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

现在,有30行,即:

  • 没有版本信息

    30 * 263会给我们7890个字节。然后添加96字节的页眉,以使用7986字节。最后,添加插槽数组的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;

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


准确地说,每页8060字节可用于用户数据。OP的数据仍低于该值。
罗杰·沃尔夫

版本信息不存在,否则30行将占用8310字节。其余的似乎是正确的。
罗杰·沃尔夫,

@RogerWolf是的,这里有“版本信息”。因此,是的,30行确实需要8310字节。这就是为什么那30行实际上不能全部放入一页中的原因,因为OP会通过OP所使用的任何测试过程来相信它。但是那个测试过程是错误的。页面上仅可容纳29行。我已经确认了这一点(甚至使用SQL Server 2012)。
所罗门·鲁茨基

您是否尝试在未启用RCSI的数据库上运行测试tempdb?我能够重现OP提供的确切数字。
罗杰·沃尔夫

@RogerWolf我正在使用的数据库未启用RCSI,但已设置为ALLOW_SNAPSHOT_ISOLATION。我还尝试了一下,tempdb发现其中没有“版本信息”,因此30行确实合适。我将更新以添加新信息。
所罗门·鲁兹基

3

数据页的实际结构非常复杂。虽然通常说每页8060字节可用于用户数据,但仍有一些额外开销未在任何地方计算,这导致了这种行为。

但是,您可能已经注意到,SQL Server实际上给您提示,第31行将不适合该页面。为了使下一行适合同一页面,该avg_page_space_used_in_percent值应低于100%-(100/31)= 96.774194,在您的情况下,该值应高于该值。

PS:我相信我在Kalen Delaney撰写的一本“ SQL Server内部知识”书中看到了详细的数据页面结构的字节解释,但这已经快10年了,请原谅我不记得任何更多的细节了。此外,页面结构趋于随版本而变化。


1
否。唯一符仅添加到重复的键行中。每个唯一键值的第一行不包含额外的4字节唯一符。
所罗门·鲁兹基

@srutzky,显然你是对的。从来没有想过SQL Server将允许可变宽度的键。这很丑。高效,是的,但是很难看。
罗杰·沃尔夫
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.