[转载] 存储引擎揭秘:基本结构之四——IAM页,IAM链和存储单元

1、SQL Server2000中的IAM链

IAM(Index Allocation Map,索引分配映射)页用来跟踪单个文件中约4G大小的空间,跟踪的空间是按4G字节对齐。被跟踪的这4G大小的页被称为“GAM区间”(GAM Interval)。IAM页所跟踪的GAM区间中的空间是属于同一实体的(这里,我选择的是“实体”而没有使用SQL Server中的“对象”)。下面我们将了解IAM页的结构。

因为IAM只是跟踪单个文件里的一个GAM区间里的空间。若一个数据库包括多个文件,或者一些文件大小超过4G,并且实体分配的空间恰好跨越多个文件或跨越一个文件的多个GAM区间,那么你就会看到为了跟踪单个实体的所有空间是如何使用多个IAM页的了。

这时就要用到IAM链了。它是一个IAM页的链表,该链表将所有用来跟踪同一实体的分配空间的所有IAM页链接在一起。该链表并不排序,当需要一个新的IAM页时便添加进链表。链表中的IAM页是编号的,这个编号也是按添加时的顺序增加的。

到底是谁使用IAM链?——实体。这个概念在SQL SERVER 2000和2005中区别很大,所以我要用两篇文章来介绍它。

在SQL Server2000中,下面每个实体都有一个IAM链表:

1) 堆或聚集索引

a) 一个表只能选其一,不能两者皆有。

b) 它们的索引ID分别为0和 1。

2) 非聚集索引

a) 它的索引ID从2到250。

3) 文本存储

a) 对于堆或者聚集索引中的LOB列

b) 有时也被称为“文本索引”

c) 它拥有一个固定的索引ID值255。

很简单吧?我总结为:在SQL SERVER 2000中,每一个索引一个IAM链(如果你还记得IAM叫“索引分配映射”的话,我觉得还是很贴切的)。

到底IAM页是如何跟踪GAM区间中空间使用的呢?每个IAM页有两个纪录(record):IAM头部和位图。

     IAM头部记录着IAM链的一些元数据,包括:

1)    IAM页映射的GAM区间(译注:开始的页号)

2)    IAM页在IAM链表中的顺序号

a) 一个新的页增加到链表中时,该序号便增加1

b) 这些页使用页的向前和向后指针(在页头部,不在IAM头部)链接的。并且链接时是严格按顺序号的。

3) 链表映射的索引的对象和索引ID

a)    IAM链中所有的IAM页(和所有分配给该索引的页)都在头部有同样的对象和索引ID。

     IAM页头部还有一个单页的数组。这只是在IAM链中的第一个IAM页中,用来跟踪分配给索引的页而不是区(一个区是8个连续的页)的。(译注:给索引分配空间,前8页是按页在混合区中分配的;超过8个页后才开始按区分配)

位图占用IAM页剩下的空间,每一位表示GAM区间中的每一个区。如果区被分配给该索引,那么对应位就置1,否则为0。很明显,为不同索引映射同一GAM区域的两个IAM页不可能有相同的位被置上。

你可以使用DBCC PAGE命令来查看IAM页的内容。关于IAM链,还需要注意一些事:

1) 它们不是自引用的(也就是说他们并不跟踪IAM页自己使用的空间)

2) 如果一些数据删除了,从而导致空间被收回,GAM区间中没有任何空间再被分配给索引了,但是IAM页是不会被删除的。

3) 只有下面的操作会将页从IAM链表中删掉:TRUNCATE TABLE,DROP TABLE,还有DBCC CHECKDB的一些修补动作。

4)    DBCC INDEXDEFRAG会为索引拷贝一份IAM链,并且按文件和GAM区间排序以便计算出下面每个文件的下一个区/页。

5)    IAM链的修补工作是异常复杂的。

2、SQL Server2005中的IAM链

现在,我们开始IAM链的第二部分。

昨天,我解释了什么是IAM链和在SQL SERVER 2000中是如何和索引相配对的。一个表可以有一个堆或聚集索引,可有249个非聚集索引,可有一个LOB索引(一般称为TEXT索引),这就是说SQL SERVER 2000中,一个表中最多有251个IAM链。

在SQL SERVER 2005中,IAM链和IAM页与SQL SERVER 2000中是一模一样的,但是现在一个表可以拥有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分配单元

让我们来看看引起变化的SQL SERVER 2005的新增的3个特性,这些特性增加了每个表的IAM链数目的潜力。

一.包含列

这项功能可以在非聚集索引的叶节点中包含非键列。这条特性因为下面三个原因所以非常有用:

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数据。

二.巨行(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啊——剩下的是从哪儿来啊?

三.分区

分区给了我们1000倍的能力。可能你早就知道了,SQL SERVER 2005中新增的分区特性使表和索引能被分割成一系列的段,每个段被单独存储(更常见的是被存储在单独的文件组中)。分区需要另一文介绍。

如果表或索引的每个段或分区是单独存储的,那么每个存储就需要它自己的HOBT分配单元。当然,每个分区可以存储LOB值,所以每个分区需要一个LOB分配单元。还有每个行的行溢出特性,就像未分区时表和索引一样,每个分区中的行会溢出至SLOB分配单元中。所以每个表或索引的分区都能有3个分配单元(,也就有了3个IAM链)。

那么1000倍是从哪儿来的呢?这是因为每个表或索引可以有1000个分区。就是250索引*1000分区*3个分配单元=750000个IAM链。现实中这可能并不会发生,这只是一种可能性。

3、正文

本文是我以前发表的材料的组合,新加入一些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 查看我们创建的表:

clip_image001

通过查看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 链中没有其他的IAM 页。
  • slot 数目为2 :一个是IAM 头记录;另一个是位图。
  • 页几乎满了

IAM 页头有下面的字段:

  • sequenceNumber
    • 这是IAM 页在IAM 链中的位置值。每一个IAM 页加入到IAM 链时该值增加1 。
  • status
    • 未用
  • objectId
  • indexId
    • 在SQL SERVER 2000 及以前版本,这是IAM 页所属的对象和索引的ID ;2005 和以后的版本这两个字段未用。
  • page_count
    • 字段未用——原来是用来跟踪单页分配数组中页数目。
  • start_pg
    • IAM 页映射了一个GAM 区间。这个字段存储了映射区间的首页ID.
  • 单页分配数组( Single Page Allocations array )
    • 这些是从混合分区中分配的页的数组。这个数组只存在IAM 链中的第一个页中(因为整个IAM 链中只需要跟踪8 个单独分配的页)。

位图占用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 也可以用来跟踪另一个文件的区。

IAM 链

如果我们一直增大文件并往表中插入数据,最终我们将需要另一个IAM 页来映射下一个GAM 区间。这就是IAM 链的由来。IAM 链表用来跟踪单个实体上的空间分配。这个链表是不排序的——IAM 页按添加的顺序加入链表中,每个IAM 页有一个数值,同样是以添加顺序增加的。

“实体”的定义。到底是谁使用IAM 链?这个概念在SQL SERVER 2000 和2005 中区别很大。

在SQL Server2000 中,下面每个实体都有一个IAM 链表:

  • 堆或聚集索引
    • 一个表只能选其一,不能两者皆有。它们的索引ID 分别为0 和 1 。
  • 非聚集索引
    • 它的索引ID 从2 到250 (就是说只有249 个索引)。
  • 表的完整LOB 存储
    • 对于堆或者聚集索引中的LOB 列,有时也被称为“ 文本索引” ,它拥有一个固定的索引ID 值255 。

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 链。现实中这可能并不会发生,这只是一种可能性。

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

导航