SQL Server数据页是8KB最小I/O单位,含页头(96字节,含Page ID、m_type等元数据)、行数据(分固定/变长字段、NULL位图)和行偏移数组(页尾2字节/槽,支持O(1)行定位)。

SQL Server 的数据页是存储数据的最小 I/O 单位(默认 8KB),理解其内部结构对性能调优、故障排查和数据恢复至关重要。页中不仅存着用户数据行,还包含页头、行偏移数组、空闲空间等元信息。行格式则决定了每行数据如何组织、是否压缩、是否有变长字段等。
页头(Page Header):80 字节的控制中枢
每个数据页开头固定 96 字节(SQL Server 2005+ 实际为 96 字节,常被简称为“80 字节页头”,但含扩展字段),存放页级元数据。关键字段包括:
- Page ID:由 file_id + page_id 组成,唯一标识该页位置;
- m_type:页类型,如 1=数据页(data page)、2=索引页(index page)、8=全局分配映射页(GAM)等;
- m_freeCnt:页内剩余可用字节数(含行偏移数组空间);
- m_slotCnt:当前页中实际存储的记录数(即行数);
- m_xactReserved:用于锁升级或延迟删除的预留字节数;
- m_tornBits / m_checksum:校验机制,分别对应“撕裂修复”与页级校验和(取决于数据库选项)。
可通过 DBCC PAGE 命令查看原始页头内容,例如:DBCC PAGE('YourDB', 1, 123, 3) 中的 type=3 输出即包含完整页头解析。
行格式:堆表与聚集索引下的物理布局差异
SQL Server 支持两种主要行格式:普通行(Regular Row)和行溢出/大对象处理机制。是否启用 ROW_OVERFLOW_DATA 或 LOB_DATA 分配取决于列定义和实际长度。
- 固定长度列:按定义顺序连续存放,无额外开销;
- 变长列:统一放在行尾,前面用两个字节的「变长列偏移数组」描述各列起始位置;
- NULL 位图:每行开头有 2 字节基础头(Status Bits A),后接 NULL 位图(1 bit/列),标记该列是否为 NULL;
- 行标识符 RID:堆表中每行隐含 8 字节 RID(file:page:slot),作为唯一行定位符;聚集索引中则以聚簇键替代 RID。
当一行总长度 > 8060 字节(不含 LOB 列本身),SQL Server 会将部分变长列自动推至行溢出页(Row-Overflow Page),原位置仅保留 24 字节指针。
行偏移数组(Slot Array):页尾的“目录索引”
页末尾从后往前生长一个数组,每个条目占 2 字节,记录本页中第 n 行的起始偏移量(距页首地址)。例如 slot 0 对应第一行在页内的起始地址,slot 1 对应第二行……数组大小 = m_slotCnt × 2。
- 插入新行时,SQL Server 在空闲空间中写入数据,并在数组末尾添加对应偏移;
- 删除行时,仅将对应槽位清零,不移动其他行或调整数组顺序;
- 更新导致行变长且原地放不下时,可能触发“前移”(forwarding pointer)——原位置留 4 字节跳转指针,数据移到新位置。
这个数组让 SQL Server 能 O(1) 定位任意行,也是 DBCC PAGE 中 “OFFSET TABLE:” 区域的来源。
实战提示:何时需要关注这些底层结构?
多数业务场景无需直接操作页结构,但在以下情况值得深入:
- 排查“页面撕裂”或校验失败错误(如 823/824 错误);
- 分析页分裂频繁、填充因子异常、碎片率高问题;
- 做低级别数据恢复(如 DBCC WRITEPAGE 已禁用,但解析页仍可用于取证);
- 理解为什么 VARCHAR(MAX) 存小文本仍快于 TEXT,或为何某些 UPDATE 导致大量 forwarding stubs。
掌握页头与行格式不是为了日常开发,而是构建对 SQL Server 存储引擎的直觉——知道数据真正在磁盘上怎么躺,才能更准地判断它为什么会慢、为什么会坏。









