[转载] 存储引擎揭秘:基本结构之二——页

继续存储引擎揭秘系列,今天讨论页结构。页是用来存储记录的。一个页是数据库文件中的一个 8192 字节段。页在数据文件中开始于 0 字节,并按 8192 字节对齐。下面是一个页的基本结构图:

clip_image001

页头部

页头部大小为 96 字节。在这部分我最想做的事是使用 DBCC PAGE 来看一个页头部,然后解释一下所有的字段含义。我使用以前《 page split 》文章用的数据库,下面是 DBBC PAGE 部分输出:

DBCC TRACEON (3604)

DBCC PAGE ('pagesplittest', 1, 143, 1);

GO

m_pageId = (1:143)     m_headerVersion = 1     m_type = 1

m_typeFlagBits = 0x4   m_level = 0             m_flagBits = 0x200

m_objId (AllocUnitId.idObj) = 68   m_indexId (AllocUnitId.idInd) = 256 

Metadata: AllocUnitId = 72057594042384384

Metadata: PartitionId = 72057594038386688        Metadata: IndexId = 1

Metadata: ObjectId = 2073058421  m_prevPage = (0:0)  m_nextPage = (1:154)

pminlen = 8            m_slotCnt = 4     m_freeCnt = 4420

m_freeData = 4681      m_reservedCnt = 0   m_lsn = (18:116:25)

m_xactReserved = 0    m_xdesId = (0:0)    m_ghostRecCnt = 0

m_tornBits = 1333613242

下面是所有字段的解释(注意页中字段并不是按下面顺序存储排列的):

  • m_pageId
    • 这个字段标明了文件 ID 及页在该文件中的位置。在本例中( 1:143 )
  • m_headerVersion
    • 页头部版本。自从 7.0 以来,这个值总是为 1 。
  • m_type
    • 页类型,你可能见到的页类型如下:
      • 1 - 数据页。这种页存储堆或聚集索引的叶节点中的数据记录。
      • 2 - 索引页。这种页存储聚集索引的非叶节点 或非聚集索引的所有结点 的索引记录
      • 3 - 文本混合页。这种文本页存储小段的 LOB 值以及文本树的内部。这种页可被同一索引 / 堆的分区的 LOB 值所共享。
      • 4 - 文本树页。这种文本页存储一个单独列的大段的 LOB 值。
      • 7 - 排序页。这种页存储在一次排序操作中的中间结果。
      • 8 - GAM 页。这种页存有一个 GAM 区间(每个数据文件逻辑上被分割成约 4GB 大小的段,这个“约 4GB ”就是一个页中的位图所能表示的区)中所有区的分配信息:一个区是否已经被分配? GAM 表示全局分配映射( G lobal A llocation M ap )。第一个 GAM 页是每个文件的第 2 页。
  • 9 - SGAM 页。这种页也是存有一个 GAM 区间中所有区的分配情况:一个区是否可以分配混合页? SGAM 表示共享 GAM 。第一个 SGAM 页是每个文件的第 3 页。
  • 10 – IAM 页。这种页包含一个 GAM 区间中哪些区已分配给一个索引( SQL SERVER 2000 中)或分配单元( 2005 中)。 IAM 表示索引分配映射( Index Allocation Map )。
  • 11 - PFS 页。这种页存有一个 PFS 区间(每个数据文件逻辑上被分割成约 64MB 大小的段,这个“约 64MB ”就是一个页中的字节所能表示的页)中每个的页的分配和可用空间的信息。 PFS 表示页的可用空间。第一个 PFS 页是每个文件的第 1 页。
  • 13 - 启动页。这种页含有数据库的信息。每个数据库只有一个启动页,它是数据文件 1 的第 9 页。
  • 15 - 文件头页。这种页包含文件的信息。每个文件一个文件头页,是文件的第 0 页。
  • 16 - 差异映射页 (DIFF) 。这种页包含有自上次完整备份以来一个 GAM 区间中已发生改变的区的信息。第一个 DIFF 页是每个文件的第 6 页。
  • 17 - ML 映射页。这种页包含有自上次备份以来一个 GAM 区间中哪些区在 BULK-LOGGED 模式下发生了大容量日志操作。你为了大容量加载或重建索引而将恢复模式变为 BULK-LOGGED ,有了这种页就不用担心打断备份链了。第一个 ML 页是每个文件的第 7 页。
  • m_typeFlagBits
    • 基本未用。数据页和索引页,此字段总是 4 ;其他类型页(除了 PFS 页)该字段总是为 0 。如果一个 PFS 页的 m_typeFlagBits 为 1 ,表示 PFS 页映射的 PFS 区间中的至少有一页中有至少一个幽灵记录。
  • m_level
    • 这表示页在 B 树上的层。
    • 叶节点是 0 层,每向上加一层增加 1 ,直到根节点(即 B 树的最高点)。
    • 在 SQL SERVER 2000 中,一个聚集索引的叶节点(数据页)是 0 ,它的上面一层(索引页)也是 0 ,然后才向上增加,直到根节点。所以在 SQL SERVER 2000 中为了判断一个页是否是叶节点,你需要查看 m_type 和 m_level 两个字段。
    • 除了索引页外所有其他类型的页,其层次总是为 0 。
  • m_flagBits
    • 这包含了一些用来描述页的不同的标志。比如, 0x200 表示页上面有校验和(就像我们的例子); 0x100 表示页上面有残损页保护。
    • 一些位在 SQL SERVER 2005 中不再使用。
  • m_objId
  • m_indexId
    • 在 SQL SERVER 2000 中,这些 ID 表示本页所分配给的实际的关系对象和索引的 ID 。在 SQL SERVER 2005 中,不再是这样了。分配元数据全部改了,所以这些字段不再表示 ID 了而是表示本页所属的分配单元了。
  • m_prevPage
  • m_nextPage
    • 这是 B 树上同一层中的前一页和后一页的指针。这些字段都是 6 个字节的页 ID.
    • 索引的每层上的页都用一个双向链表按索引的逻辑顺序(就是定义的索引键)链接起来。因为存在碎片,所以指针指向的页没有必要跟当前页物理上相邻。
    • B 树上一层最左面的页的 m_prevpage 为 NULL; 最右面的页的 m_nextpage 为 NULL.
    • 堆或者只有一页的索引中,所有页的这两个指针都是 NULL.
  • pminlen
    • 页中记录的定长部分的大小。
  • m_slotCnt
    • 页中记录的个数。
  • m_freeCnt
    • 页中有多少字节的可用空间。
  • m_freeData
    • 从页开始到最后一个 记录结尾的下一字节的偏移值。如果它前面也有可用空间也没有关系。
  • m_reservedCnt
    • 由活动事务保留的可用空间的字节数。这可以防止用光所有的可用空间,以保证事务能正确回滚。改变这个值有一套复杂的算法。
  • m_lsn
    • 最后一次修改本页的日志的 LSN.
  • m_xactReserved
    • 最后一次加到 m_reservedCnt 上的数目。
  • m_xdesId
    • 最近一次加到 m_reserverdCnt 上的事务的内部 ID.
  • m_ghostRecCnt
    • 页中幽灵记录的数目。
  • m_tornBits
    • 本字段或者是页的校验和或者是残损页保护中被替换的位。这是依赖于本数据库到底是用哪种保护方式。

注意:我并没有包括以 Metadata 开头的字段,因为它们并不是页头部的一部分。在 SQL SERVER 2005 的开发过程中,我花了大量的精力来改写 DBCC PAGE 命令,为了节省每个使用者在系统表中查询对象 / 索引 ID 的时间,我在 DBCC PAGE 中进行了查询,并输出最终的结果。

记录

见我专门的文章。

行偏移数组

有一个很常见的误解是页中的记录是按逻辑顺序存储的,这是错误的。还有一种误解是一个页中所有可用空间总是维护成一块连续的段,这也是错误的。(是的,上面的图片中显示可用空间确实是一个段,这通常是当页是逐步填充时才会发生)

如果从页中删除一个记录,页上剩下的记录并不会立刻被压紧的( compact )——如果插入时需要的话会插入过程会花时间压紧的,但删除过程不会进行压紧操作的。

考虑一个完全满的页,这表示当删除发生时,机会造成页中有可用空间洞。如果一个有新记录要插入到页中,而页上的一个洞足够大,那为什么还要压紧呢?直接将记录放进去就行了。如果这个记录需要逻辑上放在所有其他记录的后面,而我们的插入位置却是在中间——这不会坏了事情吗?

不会的。因为行偏移数组是排序的,并且每次记录插入和删除后都会重排。只要行偏移数组第一个条目指向逻辑上的第一个记录,就不会有事。每个条目是两个字节的 页中偏移——所以操作行偏移数组比操作记录有效多了。只有当我们知道页中有足够的可用空间,但这些空间分散在页内,我们才需要压紧记录让可用空间变成一整段。

一个有趣的事实是,行偏移数组是从页尾部向前增长的。所以当记录压紧后,可用空间是从新行的顶部到行偏移数组的尾部。

posted on 2011-06-28 14:46  围成  阅读(337)  评论(0编辑  收藏  举报

导航