存储引擎揭秘:基本结构之四——IAM 页,IAM 链和存储单元
原文地址:
http://www.sqlskills.com/BLOGS/PAUL/post/Inside-the-Storage-Engine-IAM-pages-IAM-chains-and-allocation-units.aspx
译者:正如本文作者所言,本文是以前他的一些文章的结合,以前曾经翻译的相关文章有:
1 . 《 SQL Server2000 中的 IAM 链》
原文地址 : http://blogs.msdn.com/b/sqlserverstorageengine/archive/2006/06/24/645803.aspx
译文地址 : http://blog.csdn.net/misterliwei/archive/2010/09/25/5906035.aspx
2 . 《 SQL Server2005 中的 IAM 链》
原文地址 :
http://blogs.msdn.com/b/sqlserverstorageengine/archive/2006/06/25/under-the-covers-iam-chains-and-allocation-units-in-sql-server-2005.aspx
译文地址 : http://blog.csdn.net/misterliwei/archive/2010/09/26/5907155.aspx
正文:
本文是我以前发表的材料的组合,新加入一些DBCC PAGE 输出。
IAM 页
IAM (Index Allocation Map ,索引分配映射)页用来跟踪单个文件中约4G 大小的空间,跟踪的空间是按4G 字节对齐。被跟踪的这4G 大小的页被称为“GAM 区间” (GAM Interval )。IAM 页所跟踪的GAM 区间中的空间是属于同一实体的(这里,我选择的是“ 实体” 而没有使用SQL Server 中的“ 对象” )。
因为IAM 只是跟踪单个文件里的一个GAM 区间里的空间。若一个数据库包括多个文件,或者一些文件大小超过4G ,并且实体分配的空间恰好跨越多个文件或跨越一个文件的多个GAM 区间,那么你就会看到为了跟踪单个实体的所有空间是如何使用多个IAM 页的了。如果一个实体需要多个IAM 页来跟踪它的所有的区,那么所有的IAM 页必须链在一起,这就是IAM 链的由来。更多内容见下。
每个IAM 页有两条记录:一个IAM 页头和一个位图。让我们用DBCC PAGE 来看1 个IAM 页。我用了以前《page split 》文章用的数据库。使用DBCC IND 查看我们创建的表:
通过查看PageType 列,我们可以看到有一个IAM 页(页类型为10 )的ID 为(1:152 ):
DBCC TRACEON (3604);
GO
DBCC PAGE ('pagesplittest', 1, 152, 3);
GO
m_pageId = (1:152) m_headerVersion = 1 m_type = 10
m_typeFlagBits = 0x0 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 = (0:0)
pminlen = 90 m_slotCnt = 2 m_freeCnt = 6
m_freeData = 8182 m_reservedCnt = 0 m_lsn = (18:116:13)
m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0
m_tornBits = -1947725876
Allocation Status
GAM (1:2) = ALLOCATED SGAM (1:3) = ALLOCATED
PFS (1:1) = 0x70 IAM_PG MIXED_EXT ALLOCATED 0_PCT_FULL DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED
IAM: Header @0x620CC064 Slot 0, Offset 96
sequenceNumber = 0 status = 0x0 objectId = 0
indexId = 0 page_count = 0 start_pg = (1:0)
IAM: Single Page Allocations @0x620CC08E
Slot 0 = (1:143) Slot 1 = (1:153) Slot 2 = (1:154)
Slot 3 = (0:0) Slot 4 = (0:0) Slot 5 = (0:0)
Slot 6 = (0:0) Slot 7 = (0:0)
IAM: Extent Alloc Status Slot 1 @0x620CC0C2
(1:0) - (1:272) = NOT ALLOCATED
关于页头部需要注意:
IAM 页头有下面的字段:
位图占用IAM 页剩下的空间,每一位表示GAM 区间中的每一个区。如果区被分配给该实体,那么对应位就置1 ,否则为0 。很明显,为不同实体映射同一GAM 区域的两个IAM 页不可能有相同的位被置上——DBCC CHECKDB 会检查这个。在上面的DBCC PAGE 输出中,你可以看出没有区分配给表。你会发现输出最多到272 页所在的区—— 这是因为数据文件就这么大。我往表中插入更多的行,然后再次对IAM 页执行DBCC PAGE ,这次DBCC PAGE 输出如下:
IAM: Single Page Allocations @0x620CC08E
Slot 0 = (1:143) Slot 1 = (1:153) Slot 2 = (1:154)
Slot 3 = (1:155) Slot 4 = (1:156) Slot 5 = (1:157)
Slot 6 = (1:158) Slot 7 = (1:159)
IAM: Extent Alloc Status Slot 1 @0x620CC0C2
(1:0) - (1:152) = NOT ALLOCATED
(1:160) - (1:296) = ALLOCATED
(1:304) - (1:400) = NOT ALLOCATED
你会发现所有的单页分配数组都已经满了 , 然后转向分配统一区。第一个有效的区开始于160 页,一直到296 页开始的区。同样注意到文件一定是变大了,因为现在输出显示文件中已经有400 页了。
关于IAM 页还要注意两件事:
IAM 链
如果我们一直增大文件并往表中插入数据,最终我们将需要另一个IAM 页来映射下一个GAM 区间。这就是IAM 链的由来。IAM 链表用来跟踪单个实体上的空间分配。这个链表是不排序的——IAM 页按添加的顺序加入链表中,每个IAM 页有一个数值,同样是以添加顺序增加的。
“实体”的定义。到底是谁使用IAM 链?这个概念在SQL SERVER 2000 和2005 中区别很大。
在SQL Server2000 中,下面每个实体都有一个IAM 链表:
SQL SERVER 2000 及以前版本中每个兑现最多251 链表。我常总结为:在SQL SERVER 2000 中,每一个索引一个IAM 链(如果你还记得IAM 叫“ 索引分配映射” 的话,我觉得还是很贴切的)。
分配单元(SQL SERVER 2005 及以后版本)
现在在SQL SERVER 2005 及以后版本中,发生了一些变化。虽然IAM 链和IAM 页与以前是一模一样的,但是它们所对应的东西变了,而且现在一个表可以拥有750000 条IAM 链!现在IAM 链为三类东西映射分配空间:
1 .堆和B 树(B 树是系统用来存储索引的内部结构)
2 .LOB 数据
3 .行溢出数据
我们称这些分配空间的单元为分配单元(allocation units ),这三类分配单元的相应的内部名称为:
1 .HOBT 分配单元(发音和指环王中的霍比特人一样)
2 .LOB 分配单元
3 .SLOB 分配单元(SMALL –LOB)
对应的外部名称为:
1 .IN_ROW_DATA 分配单元
2 .LOB_DATA 分配单元
3 .ROW_OVERFLOW_DATA 分配单元
严格来说,它们不能再被称为IAM 链了,因为它们不再跟踪索引的分配空间了。只是它们还是IAM 页的链表,所以还被称为IAM 链,现在它跟踪的单元叫分配单元(allocation unit )。除了这些,和以前没有任何区别。
让我们来看看引起变化的SQL SERVER 2005 的新增的3 个特性,这些特性增加了每个表的IAM 链数目的潜力。
1 .包含列
这项功能可以在非聚集索引的叶节点中包含非键列。这条特性因为下面三个原因所以非常有用:
1 ).当查询结果包含超过16 列或者所有列的总长度大于900 字节时,它允许一个非聚集索引真正地覆盖一个查询(还记得吗?在SQL SERVER 中,一个非聚集索引键不能超过16 列且不能超过900 个字节。)。
2 ).它允许不能作为索引键的数据类型包含在非聚集索引(比如text 或者XML 类型)中。
3 ).它允许一个非聚集索引覆盖一个查询而又不需要所有的查询列都作为索引键列。因为索引键会包含在B 树的所有层的行中,所以包含列可以使得索引占用的空间更小。
举个节省空间的例子:假设有一个1 亿行的索引,其键长度为900 字节,但是实际上只有前面2 个整数需作为索引键,其它4 个固定列可以作为包含列。一个900 字节的索引,那么1 页只能包含8 行(比如,fanout 为8 )。这就是说叶节点需要12500000 页,上一层为1562500 页…… 做个汇 总,一共需要12500000 + 1562500 + 195313 + 24415 + 3052 + 382 + 48 + 6 + 1 = 14285717 页(包括叶节点以上层的1785717 页)。
如果我们使用包含列的方法使得键缩为8 个字节,那么B 树的叶节点以上层一行大小为15 字节(包含了一些行的负载,这样fanout 约为537 )。注意叶节 点的fanout 还是8 ,因为存储在叶节点上的数据是一样的。这样还是12500000 页叶节点,但是上一层结点为23278 页。所以总的 为:12500000 + 23278 + 44 + 1 = 12523323 页(包括叶节点以上层的23323 页)。和上面的900 字节的键比较,这节省了1762394 页(12% )或者13.6GB 。当然这个例 子有点夸张,但是节省空间是显而易见的。
增加包含列这种特性的主要原始是可以真正地覆盖查询。一个覆盖查询是指查询优化器知道从一个非聚集索引中得到所有查询结果,所以就没有必要使用额外的IO 从基表中查询数据就能满足查询,这是非常重大的性能节省。
现在非聚集索引有了包含列,这些列可以是LOB 数据类型。这就是说SQL SERVER 2005 再也没有必要有一个单独的LOB 分配单元(在SQL SERVER 2000 中有一个单独的text 索引)了,因为每个索引都有自己的LOB 组了。你可能会问我们为什么没有增加单独一组LOB ,然后让各个索引和基表指向这 些LOB 列?我们确实曾经考虑过,但发现它会使问题更加复杂。
所以,有了这个特性,每个索引需要两个分配单元—— 一个是为数据或索引(HOBT 分配单元),一个为任意的LOB 数据。
2 .巨行(Large Rows )
一个一直折磨架构设计师的问题是表的行大小的8060 字节限制。在SQL SERVER 2005 中,我们去除了这个限制。我们解决这个问题的方法是当行的长度太长以至于不能放在一个单独的页中,允许系统把变化长度列(如varchar, sqlvariant )挤出行去。
那么这些列的值被挤到什么地方去呢?我们有效地将它转换成小的LOB 列。行中列值由一个指向挤出列值的16 字节指针所代替,挤出列就好像是一个LOB 值被 存储在一个独立的分配单元—— 行溢出分配单元(SLOB )中。这些值和正规的LOB 值一样存储在text 页中,只不过用的是一个独立的分配单元,只要当行 中有一列被挤出时就会创建SLOB 分配单元。
这种巨行特性同样适用于非聚集索引。如果你考虑在非聚集索引中使用包含列,那么你的非聚集索引很容易超过一个页的大小。如果不在非聚集索引上使用行溢出特性,那么我们将刚摆脱了900 字节的限制,又会有8060 字节的限制了。
现在有了这些特性,每个索引能有三个分配单元——HOBT 、LOB 和SLOB 。即使这样,一个表最多也就是有750 个IAM 链啊(记住IAM 链现在用来映 射分配单元了,所以250 个索引*3 个分配单元=750 个IAM 链)。但是我前面提到每个表有750000 个IAM 啊—— 剩下的是从哪儿来啊?
3 .分区
分区给了我们1000 倍的能力。可能你早就知道了,SQL SERVER 2005 中新增的分区特性使表和索引能被分割成一系列的段,每个段被单独存储(更常见的是被存储在单独的文件组中)。分区需要另一文介绍。
如果表或索引的每个段或分区是单独存储的,那么每个存储就需要它自己的HOBT 分配单元。当然,每个分区可以存储LOB 值,所以每个分区需要一个LOB 分 配单元。还有每个行的行溢出特性,就像未分区时表和索引一样,每个分区中的行会溢出至SLOB 分配单元中。所以每个表或索引的分区都能有3 个分配单元(, 也就有了3 个IAM 链)。
那么1000 倍是从哪儿来的呢?这是因为每个表或索引可以有1000 个分区。就是250 索引*1000 分区*3 个分配单元=750000 个IAM 链。现实中这可能并不会发生,这只是一种可能性。