http://www.windbi.com/showtopic-1682.aspx
存储引擎内幕:IAM页、IAM链及分配单元
--王成辉翻译整理,转贴请注明出自微软BI开拓者www.windbi.com
--原帖地址
以前写过一系列使用DBCC Page的贴子,这是接下来的另一篇。
IAM页
一个IAM页(索引分配页)跟踪单个文件里将近4GB的空间,以4GB为界。这4GB的数据称为“GAM间隔”。一个IAM页跟踪属于单个实体(这里我小心的选择我的用语,并且不使用SQL Server有诸如“对象”等之类隐含意义的词语)的特定GAM间隔内的扩展盘区。
一个IAM只跟踪单个文件里单个GAM的空间,所以如果数据库有多个文件、或者一些文件不止4GB,并且实体从多个文件或单个文件的多个GAM间隔分配空间的话,那么你可以看到每个实体要跟踪它使用所有空间需要多少IAM页。如果一个实体需要多个IAM页来跟踪所有扩展盘区的话,那么这些IAM必须链接在一起。这就是所说的IAM链。更多内容请往下读。
每个IAM页有2条记录,一个IAM页头和位图。让我们用DBCC Page来看看,我使用这个帖子里的数据库。在我们创建的表上执行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
页头自身要注意的一些事情:
- 页类型为10,正如我们预料到的
- 前一个和下一个页指针为NULL,因为该页链里没有其他IAM页
- 槽数为2——一个是IAM页头记录,另一个是位图自身
- 页几乎全部填满
IAM页头有下面的字段:
- sequenceNumber
- 这是IAM链里IAM页的位置。对于每个添加到IAM链里的页会按1增加。
- status
- 这个未使用。
- objectId
- indexId
- 在SQLServer2000及以前版本里,这些是包含IAM页的对象和索引ID。在SQLServer2005及以后的版本里未使用。
- page_count
- 这个未使用——它用来记录单个页分配数组里被跟踪的页ID的数量。
- start_pg
- 这是页映射的GAM间隔,它存储了映射间隔里的第一页的ID。
- Single Page Allocations array
- 这些是从混合扩展盘区已分配的页。该数组仅用于链里第一个IAM页(整个IAM链只需跟踪最多8个单个页的分配)。
位图占用了IAM页余下的空间,并且在GAM间隔里每个扩展盘区都有一位。如果扩展盘区被分配给实体,位就被设置,如果没有,就清除。显然,对于映射不同实体相同GAM间隔的2个IAM页不可能有相同的位设置——DBCC CHECKDB会检测这个。在上面DBCCPAGE的输出结果中,你可以看到没有扩展盘区分配给表。你会注意到输出结果仅从文件里的272页开始进入扩展盘区——这是因为数据文件只有那么大。我插入更多的记录到表里,然后做IAM页的另一个DBCC PAGE。这次DBCCPAGE输出结果包括:
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
你可以看到整个单页分配数组(single-page allocation array)都填满了,然后分配切换到统一扩展盘区。第一个可用的扩展盘区一定是从160页开始的,并且从开始到296页所有的扩展盘区现在已经分配。也要注意文件肯定已经增长了,因为输出结果现在达到文件里的400页了。
IAM页需要注意的两点:
- 它们自身有来自混合扩展盘区里的单页分配,并且任何地方都不记录。
- 它们可以从任何文件里分配来跟踪任何其它文件里的扩展盘区。
IAM链
如果我们继续增长文件并填充表,那么最终我们需要另一个IAM页来映射下一个GAM间隔。这样IAM链就产生了。它是跟踪单个实体空间分配的IAM页的链接列表。这个链接列表根本不会存储——IAM按照它们需要的顺序增加。再说一次,列表内的IAM页按照它们添加到列表内的顺序进行编号。
实体的定义——是什么在使用IAM链?这在SQL2000和2005中有很大的不同。
在SQL Server2000中,单个IAM链用于以下情况:
- 堆或聚集索引
- 一个表只可能是堆表或聚集表(有聚集索引的表),不可能二者都是,对应的索引ID分别为0、1。
- 非聚集索引
- 索引ID从2到250(即你只可能有249个非聚集索引)
- 表的完整的大对象存储
- 针对堆表或聚集表里的大对象的列(text、ntext、image)。有时候称作“文本索引”并且有一个固定的索引ID,即255。
这在SQL Server2000及以前版本里就决定了每个对象最多有251个IAM链。在SQL2000里通常我概括说,每个索引有一个IAM链(这是挺合适的,如果你记住IAM代表索引分配映射的话)。
分配单元(SQL Server 2005以及后续版本)
在SQL Server 2005以及后续版本里,有了很多变化。IAM链和IAM页除了它们代表的意思不同外,其余完全相同。表现在可以有高达750000个IAM链!IAM链现在针对3种对象来映射空间分配:
- 堆和B树(B树是用于存储索引的内部结构)
- LOB数据
- 行溢出数据
我们现在称这些跟踪的空间分配的单元为分配单元(allocation units)。分配单元的这3种类型的对应的内部名称分别为:
- hobt(Heap或B-Tree,即堆或B树)分配单元
- LOB分配单元
- SLOB分配单元(Small-LOB或Short-LOB,即小LOB或短LOB)
对应的外部名称分别为:
- IN_ROW_DATA分配单元
- LOB_DATA分配单元
- ROW_OVERFLOW_DATA分配单元
它们真的不可能继续称为IAM链了,因为它们不再跟踪索引的空间分配了。然而,它们是IAM页的链仍然被称作IAM链,而且跟踪的单元现在称为分配单元。抛开这点来说,就没什么不同了。
让我们快速的来看看SQL Server 2005里这3个新的特点,这些改变是必要的,并且也提升了每个表潜在IAM链的数量。
包含列
非聚集索引有能力在其叶级包含非键列:
- 它允许非聚集索引真正覆盖那些查询结果包括多于16列或者查询结果里列长度的总和大于900字节(记住非聚集索引受限于16列和900字节)。
- 它允许包含那些由于数据类型(例如:varchar(max)或XML)的原因而在非聚集索引里不可能作为索引键的一部分的列。
- 它允许非聚集索引覆盖查询而不必把所有列都包含在索引键中。因为索引键会包含在B树所有层的记录里,所以这让索引变得更小。
节省空间的一个例子:考虑一个有一亿行的索引,键长为900字节,但仅开头2个整型键真正是索引键所需要的,其它4个固定长度列可以作为包含列存储在索引里。对于900字节的键,每个数据库页可以容纳8行()。这意味着在叶级将会有12500000个页,B树里的上一层有1562500个页,以此类推,得到的总数为12500000+1562500+195313+24415+3052+382+48+6+1=14285717页(即有1785717存储在B树叶级的较上层)。
如果我们使用包含列的方式,那么键的尺寸缩小到8字节,而且对于行的开销而言,在B树最高层的行长度降到15个字节。注意在叶级的扇出仍然会是8,因为叶级每行存储的数据的量是相同的。所以这意味着在叶级会有12500000页,紧接的上一层有23278页,依此类推,总共有12500000+23278+44+1=12523323页(其中23323页用来存储B树叶级以上的页)。和900字节键的全部大小相比,这节约了12%即1762394页,大约13.6G!显然,这有点勉强,但你可以看到空间节省是怎样发生的。
添加这个特点的主要原因是为了使覆盖查询真的能起作用。覆盖查询是查询优化器知道它能从非聚集索引里得到所有查询结果的查询,而且不必在基表里查找数据而增加额外的IO就能满足查询——显著的性能提升。
现在非聚集索引可以有包含列,而且这些列可以是LOB数据类型(但仅限于SQL2005新的数据类型如varchar(max)、nvarchar(max)、varbinary(max)和XML)。这意味着不再可能有单个的LOB分配单元了(在SQL2000里是单个文本索引的情形),因为每个索引都可以有它自己的LOB集。现在你可能会问对于提及的各个索引以及基表为什么不只是单个LOB集呢。只是我们考虑在SQL2005的开发期间这会变得更加复杂。
这样,对于新的特点而言,每个索引需要2个分配单元——一个针对数据或索引记录(hobt分配单元)、另一个针对LOB数据。
很大的行
困扰架构设计者很长时间的一件事情就是表的行大小受8060的限制,而这个限制在SQL2005里解决了。方法是允许可变长的列(例如varchar,sqlvariant)在行尺寸太大以至于单个页内不能存储时把它推到行外。
但这些被推出去的列值存在哪儿呢?它们被有效地转为小LOB列。行内的列值用一个16字节的指针来代替被推出去的行外列值,它的LOB值存储在独立的分配单元里——行溢出(或SLOB)分配单元。这些值仅使用独立的分配单元以和常规LOB值一样的方式来存储在text页里。SLOB分配单元仅在第一个列值被推出行外的时候创建。
这个特点也适合于非聚集索引——如果你考虑非聚集索引有包含列的能力的话,那么就可以很容易的让非聚集索引的行在一个页里存不下。为了除去900字节的限制,通过不扩展非聚集索引的行溢出特征来用8060字节的限制来代替它,也是很短视的。
现在,该新特点的另外一点,每个索引最多可以有3个分配单元——hobt、LOB、SLOB。这样的话,每个表最多仅可以有750个IAM链(记住IAM链现在映射分配单元的存储分配,所以250个索引×3个分配单元=750个IAM链)。但我早先说了每个表有750000个IAM链——其余的来自哪里呢?
分区
这为我们提供了1000倍的IAM链。正如我们已经知道的,分区是一个新的特点,它允许表和索引按范围分成一系列的部分,每部分单独存储(最通常的情况是单独的文件组)。分区以后作为一个单独的主题来讨论。
如果表或索引的每个范围或分区分区单独存储的话,那么各自都需要自己的hobt分配单元。当然,由于每个分区相连的LOB值需要存储,素衣所以每个分区也需要LOB分配单元。同时,行溢出特点是针对行的,所以每个分区里的行会溢出到SLOB分配单元里,就像非分区表和索引一样。这样表或索引的每个分区可能最多有3个分配单元(因而有3个IAM链)。
然而,这个1000倍来自于哪里呢?每个表或索引最多可以有1000个分区。这给了我们250个索引×1000个分区×3个分配单元=750000个IAM链。实际上这可能不会发生,但它是有可能的。