存储引擎-存储结构之二:页
说理论,总是枯燥的,先来段搞笑视频,清清脑
模特兒走秀摔倒集錦:http://947kan.com/video/player-53088-0-0.html
原文地址:
http://www.sqlskills.com/BLOGS/PAUL/post/Inside-the-Storage-Engine-Anatomy-of-a-page.aspx
继续存储引擎揭秘系列,今天讨论页结构。页是用来存储记录的。一个页是数据库文件中的一个 8192 字节段。页在数据文件中开始于 0 字节,并按 8192 字节对齐。下面是一个页的基本结构图:
页面分成个部分:BUFFER、PAGEHEADER、DATA、OFFSET TABLE
BUFFER:显示了指定页面的缓冲信息。由于它是一个内存中结构,所以仅当页面处于内存中时候才有效.
PAGEHEADER:显示指定页面的所有报头字段信息。
DATA:显示每行数据的具体存储.
OFFSET TABLE:显示了所有行偏移矩阵的内容.
页头部
页头部大小为 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 )——如果插入时需要的话会插入过程会花时间压紧的,但删除过程不会进行压紧操作的。
考虑一个完全满的页,这表示当删除发生时,机会造成页中有可用空间洞。如果一个有新记录要插入到页中,而页上的一个洞足够大,那为什么还要压紧呢?直接将记录放进去就行了。如果这个记录需要逻辑上放在所有其他记录的后面,而我们的插入位置却是在中间——这不会坏了事情吗?
不 会的。因为行偏移数组是排序的,并且每次记录插入和删除后都会重排。只要行偏移数组第一个条目指向逻辑上的第一个记录,就不会有事。每个条目是两个字节的 页中偏移——所以操作行偏移数组比操作记录有效多了。只有当我们知道页中有足够的可用空间,但这些空间分散在页内,我们才需要压紧记录让可用空间变成一整 段。
一个有趣的事实是,行偏移数组是从页尾部向前增长的。所以当记录压紧后,可用空间是从新行的顶部到行偏移数组的尾部。