了解 sp_spaceused 值:保留值和 index_size

Understanding sp_spaceused values: reserved and index_size

我是运行下面的简单脚本:

create table MyTable (filler char(10))
go

insert into MyTable (filler) values ('a')
go 1

exec sp_spaceused MyTable
go

drop table MyTable
go

得到如下结果:

rows   reserved   data   index_size    unused
------ ---------- ------ -----------   -------
1      72 KB      8 KB   8 KB          56 KB

我的问题:

  1. 为什么要保留 72 KB?

  2. 如果 table 甚至没有索引,为什么 index_size 是 8 KB?

编辑:

我想补充一点: 稍微更改脚本时:

create table MyTable (filler char(69))
go

insert into MyTable (filler) values ('a')
go 100

我得到:

rows   reserved   data   index_size    unused
------ ---------- ------ -----------   -------
100    72 KB      16 KB   8 KB          48 KB

请注意,将 filler 的大小定义为 68 字节(并插入 100 行)仍然会得到 8KB 作为 data 值(我们可以继续并将其设置为 148 字节,这将导致在另一个 8KB 的增量中,即到 24KB)。

你能帮我分解一下计算吗?如果(显然)只使用了 6,900 字节,那么增加 8KB 的原因是什么?

编辑#2: 这是 DBCC PAGE:

的结果
PAGE: (1:4392)


BUFFER:


BUF @0x00000000061A78C0

bpage = 0x00000001EF3A8000          bhash = 0x0000000000000000          bpageno = (1:4392)
bdbid = 6                           breferences = 0                     bcputicks = 0
bsampleCount = 0                    bUse1 = 18482                       bstat = 0x9
blog = 0x15ab215a                   bnext = 0x0000000000000000          bDirtyContext = 0x0000000000000000
bstat2 = 0x0                        

PAGE HEADER:


Page @0x00000001EF3A8000

m_pageId = (1:4392)                 m_headerVersion = 1                 m_type = 1
m_typeFlagBits = 0x0                m_level = 0                         m_flagBits = 0x8200
m_objId (AllocUnitId.idObj) = 260   m_indexId (AllocUnitId.idInd) = 256 
Metadata: AllocUnitId = 72057594054967296                                
Metadata: PartitionId = 72057594048151552                                Metadata: IndexId = 0
Metadata: ObjectId = 1698105090     m_prevPage = (0:0)                  m_nextPage = (0:0)
pminlen = 72                        m_slotCnt = 100                     m_freeCnt = 396
m_freeData = 7596                   m_reservedCnt = 0                   m_lsn = (55:8224:2)
m_xactReserved = 0                  m_xdesId = (0:0)                    m_ghostRecCnt = 0
m_tornBits = -2116084714            DB Frag ID = 1                      

Allocation Status

GAM (1:2) = ALLOCATED               SGAM (1:3) = NOT ALLOCATED          PFS (1:1) = 0x44 ALLOCATED 100_PCT_FULL
DIFF (1:6) = CHANGED                ML (1:7) = NOT MIN_LOGGED           

Slot 0 Offset 0x60 Length 75

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP    Record Size = 75

Memory Dump @0x0000000012A3A060

0000000000000000:   10004800 61202020 20202020 20202020 20202020  ..H.a               
0000000000000014:   20202020 20202020 20202020 20202020 20202020                      
0000000000000028:   20202020 20202020 20202020 20202020 20202020                      
000000000000003C:   20202020 20202020 20202020 010000                         ...

Slot 0 Column 1 Offset 0x4 Length 68 Length (physical) 68

filler = a                                                                   

-- NOTE: The structure of each Slot is identical to that of Slot #0, so we can simply jump to slot 99:

Slot 99 Offset 0x1d61 Length 75

Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP    Record Size = 75

Memory Dump @0x0000000012A3BD61

0000000000000000:   10004800 61202020 20202020 20202020 20202020  ..H.a               
0000000000000014:   20202020 20202020 20202020 20202020 20202020                      
0000000000000028:   20202020 20202020 20202020 20202020 20202020                      
000000000000003C:   20202020 20202020 20202020 010000                         ...

Slot 99 Column 1 Offset 0x4 Length 68 Length (physical) 68

filler = a 

所以我们可以看到最后一个插槽在 7521 字节之后开始,加上它的大小得到 7,596 字节。如果我们加上槽数组的大小(其中每个指针为 2 个字节),我们将得到 7,796 个字节。

但是,我们需要达到 8,192 字节才能填满页面。少了什么东西?

保留的 72K space 包括一个 64K 的范围(8 个页面,每个 8K)加上 8K IAM 页面开销。在这 72K 中,实际上只使用了 IAM 页和单个数据页。 sp_space_used 报告 index_size 中的 IAM 页面,尽管从技术上讲这不是索引。您可以使用未记录的 sys.dm_db_database_page_allocations TVF(仅在测试系统上使用)查看这些详细信息:

SELECT extent_file_id, extent_page_id, page_type_desc
FROM sys.dm_db_database_page_allocations(DB_ID(), OBJECT_ID(N'dbo.MyTable'), 0, 1, 'DETAILED');

此数据库显然将 MIXED_PAGE_ALLOCATION 数据库选项设置为 OFF,因此最初分配了一个完整的 64K 范围。如果该选项为 ON,则单个数据页将从混合盘区分配,而不是专用于 table 的 64K 盘区。在那种情况下分配的 space 将是 16K - 一个 8K 的单个数据页加上 IAM 页。

尽管混合区确实减少了 space 对小 table 的要求(低于 64K),但混合区有更多的开销并且可能导致高并发工作负载中的分配争用,因此默认情况下它是关闭的在 SQL 2016 年以后。在较旧的 SQL 版本中,混合区分配默认处于启用状态,可以在服务器级别使用跟踪标志 1118 将其关闭。

您可以在sys.databases中看到混合范围设置:

SELECT name, is_mixed_page_allocation_on
FROM sys.databases;

要切换设置:

ALTER DATABASE Test
    SET MIXED_PAGE_ALLOCATION ON;

编辑 1:

数据页中的

Space 包括页面本身的开销以及页面内的记录。此开销加上用户数据所需的 space 将决定一个页面可以容纳多少行以及存储给定行数所需的数据页数。有关该开销的详细信息,请参阅 Paul Randal 的 anatomy of a page and anatomy of a record 文章。

编辑 2:

来自您的后续评论:

7998 bytes, so there are more 194 bytes to go for the next allocation. What am I missing?

我几乎从不使用堆,但正如您在页面转储中所见,此页面的相关 PFS(页面空闲 space)分配状态为 100% 满。根据 Kalen Delaney's Microsoft SQL Server 2012 Internals 书,PFS 状态实际上是这些范围的 3 位掩码:

  • 000: 空
  • 001:1-50% 已满
  • 010:51-80% 已满
  • 011:81-95% 已满
  • 100:96-100% 满

所以看起来一旦堆页面充满度超过 96% 的阈值,它就被认为是 100% 满,并且分配了一个新页面。请注意,这不会发生在具有聚集索引的 table 上,因为新行的页面首先由 CI 键确定,并且仅当新页面无法放入该页面时才分配新页面全部。避免堆的另一个原因。