谈到GAM和SGAM,我们不得不从数据库的页和区说起。
https://blog.csdn.net/snowfoxmonitor/article/details/49991015
一个数据库由用户定义的空间构成,这些空间用来永久存储用户对象,例如数据库管理信息、表和索引。这些空间被分配在一个或多个操作系统文件中。
当我们创建一个数据库的时候,例如以缺省的方式CREATE DATABASE TESTDB,SQLServer自动帮我们创建好如下两个数据库文件。
这两个数据文件是实实在在的操作系统文件,其中一个是叫行数据文件,用来存储数据库的各种对象,另外一个是日志文件,从来记录数据变化的过程。
从逻辑角度而言,数据库的最小存储单位为页即8kb。
数据库被分成若干逻辑页面(每个页面8KB),并且在每个文件中,所有页面都被连续地从0到x编号,其中x是由文件的大小决定的。我们可以通过指定一个数据库ID、一个文件ID、一个页码来引用任何一个数据页。每个数据页则用来存储表和索引,以及相关的数据库管理信息。
我们顺着上面数据文件的路径可以找到该文件,观察一下新建的数据文件的大小为:
2.18 MB(2,293,760 字节)=2,293,760b/8kb=280个页面=35个区
数据库进行空间管理的最小单位为区(extents)。
一个区由8个逻辑上连续的页面组成(64KB的空间)。为了能够更有效地分配空间,SQL Server 2008不会为少量的数据向数据表分配整区的空间。SQL Server 2008有两种类型的区。
统一类型的区 这些区为单个对象所有,区中所有的8个数据页只能被所属对象使用。
混合类型的区 这些区能为最多8个对象共享。
SQL Server为新的表或索引从混合类型的区中分配页面。当该表或索引增长到8个页面时,以后所有的分配都使用统一类型的区。
当一张表或一个索引需要更多的空间时,SQL Server需要找到能够用来分配的空间。如果该表或索引整体仍然少于8个页面,SQL Server必须找到能够用来分配的混合类型区构成的空间。如果表或索引有8个页面或更大,SQL Server必须找到一个自由的统一类型的区。
SQL Server使用两种特殊类型的页面来记录哪些区已经被分配出去了,哪些类型(混合类型或统一类型)的区可供使用:
全局分配映射(Global Allocation Map,GAM)页面 这些页面记录了哪些区已经被分配并用作何种用途。一个GAM页面在它所覆盖空间里针对每一个区都有一个数据位。如果数据位为0,那么对应的区正在使用;如果该数据位为1,那么该区为自由区。一个GAM页面除了页面头部和其他一些需要记入的开销大概有8000字节或者说64 000位空间可用,所以每个GAM页面可以覆盖64 000个区,也就是大约4GB的数据。这意味着一个文件的每4GB空间对应一个GAM页面。
共享全局分配映射(Shared Global Allocation Map,SGAM)页面 这些页面记录了哪些区当前被用作混合类型的区,并且这些区需含有至少一个未使用的页面。就像一个GAM页面,每一个SGAM页面覆盖了大约64 000个区,也就是大约4GB的数据。一个SGAM页面在它所覆盖空间里针对每一个区都有一个数据位。如果数据位为1,那么对应的被使用的区为混合类型,并且该区有一些自由页面;如果数据位为0,那么对应的区不是一个混合类型的区,或者虽然是一个混合类型的区,但是所有的页面都已被使用了。
表4-2显示了基于每一个区当前的使用情况,在GAM和SGAM中该区所对应的比特位模式。
区的当前使用情况 | GAM比特位设置 | SGAM比特位设置 |
自由,未使用 | 1 | 0 |
统一类型或已全部使用的混合区 | 0 | 0 |
含有自由页面的混合区 | 0 | 1 |
如果SQL Server需要找到一个新的完全没有使用的区,那么它可以使用任何一个在GAM页面中对应的比特位值为1的区。如果SQL Server需要找到一个有着可用空间(有一个或多个自由页面)的混合类型的区,那么它可以寻找一个对应的GAM中的值为0、SGAM中的值为1的区。如果不存在有可用空间的混合类型的区,SQL Server会使用GAM页面来寻找一个全新的区并将其分配为混合类型的区,然后使用该区中的一页。如果根本没有自由区,那么这个文件已经满了。
SQL Server能够迅速地锁定一个文件中的GAM页面,因为它总是位于任何数据库文件的第三页上(页码为2)。SGAM页面是在第四页上(页码为3)。下一个GAM页面出现在第一个GAM页面(页码为2)以后的每511 230个页面中,并且下一个SGAM页面出现在第一个SGAM页面(页码为3)以后的每511 230个页面中。每一个数据库文件的页码为0的页面是文件头页面,并且每个文件仅有一页。页码0是头文件页,页码1是页面自由空间页(Page Free Space,PFS)。
在SQLServer2008的每一个数据库中的前八页顺序都是固定的。
第0页 | 第1页 | 第2页 | 第3页 | 第4页 | 第5页 | 第6页 | 第7页 |
m_type=15 | m_type=11 | m_type=8 | m_type=9 | m_type=0 | m_type=0 | m_type=16 | m_type=17 |
头文件页 | PFS页 | GAM页 | SGAM页 | 保留页 | 保留页 | DCM页 | BCM页 |
除了第9页为数据库的BOOT页以外,从第8页到第173页为SQLServer2008内部系统表的相关存储信息,然后从第174页到第279页为未分配页面。因为第一页从0开始,所以刚好280页,即和我们看到的数据库数据文件的大小完全相等。
第8页 | 第9页 | 第10页 | 第N页 | 第173页 | 第279页 |
m_type=1 | m_type=13 | m_type in (1,2,10) | N/A | ||
Data页 | Boot页 | 主要为内部系统表相关信息 | 未分配 |
以下截图是通过SQLServer2008的InternalsViewer插件看到的整体页面结构,该插件是从http://www.SQLInernalsViewer.com网站下载的,分为不同的.net版本。
备注:TESTDB为新创建的空数据库,没有任何用户自定义对象,直到有建表脚本为止;
关于数据库页类型如下所示:
类型 | 页面类型名称 | 页面类型描述 |
1 | Data page | 堆表和聚集索引的叶子节点数据 |
2 | Index page | 聚集索引的非叶子节点和非聚集索引的所有索引记录 |
3 | Text mixed page | A text page that holds small chunks of LOB values plus internal parts of text tree. These can be shared between LOB values in the same partition of an index or heap. |
4 | Text tree page | A text page that holds large chunks of LOB values from a single column value. |
7 | Sort page | 排序时所用到的临时页,排序中间操作存储数据用的。 |
8 | GAM page | 全局分配映射(Global Allocation Map,GAM)页面 这些页面记录了哪些区已经被分配并用作何种用途。 |
9 | SGAM page | 共享全局分配映射(Shared Global Allocation Map,GAM)页面 这些页面记录了哪些区当前被用作混合类型的区,并且这些区需含有至少一个未使用的页面。 |
10 | IAM page. | 有关每个分配单元中表或索引所使用的区的信息 |
11 | PFS page. | 有关页分配和页的可用空间的信息 |
13 | boot page. | 记录了关于数据库的信息,仅存于每个数据库的第9页 |
15 | file header page. | 记录了关于数据库文件的信息,存于每个数据库文件的第0页 |
16 | DCM page | 记录自从上次全备以来的数据改变的页面,以备差异备份 |
17 | BCM page. | 有关每个分配单元中自最后一条 BACKUP LOG 语句之后的大容量操作所修改的区的信息 |
实际上SQLServer还包括一些未公开的页面类型,例如type19,type 14等等。
本章我们主要介绍GAM页和SGAM页,其他页面类型会稍后介绍。
那么如何查看页面信息呢,从SQLServer2000起便开始提供了一个读取数据页结构的命令DBCC Page。该命令为非文档化的命令,具体如下:
DBCC Page({dbid|dbname},filenum,pagenum[,printopt])
具体参数描述如下:
dbid 包含页面的数据库ID
dbname 包含页面的数据库的名称
filenum 包含页面的文件编号
pagenum 文件内的页面
printopt 可选的输出选项;选用其中一个值:
0:默认值,输出缓冲区的标题和页面标题
1:输出缓冲区的标题、页面标题(分别输出每一行),以及行偏移量表
2:输出缓冲区的标题、页面标题(整体输出页面),以及行偏移量表
3:输出缓冲区的标题、页面标题(分别输出每一行),以及行偏移量表;每一行后跟分别列出的它的列值
如果要想看到这些输出的结果,还需要设置DBCC TRACEON(3604)。
如前文所述,GAM页一定存在于该数据库的第二个页面,SGAM页则一定存在于该数据库的第三个页面;而每一个数据库都会存在文件编号为1的数据库文件,所以我们执行以下命令即可。
DBCC TRACEON(3604) DBCC PAGE(TESTDB,1,2,1) —查看GAM页信息 DBCC PAGE(TESTDB,1,3,1) —查看SGAM页信息 DBCC PAGE(TESTDB,1,2,2) —查看GAM页信息和整体输出页面 DBCC PAGE(TESTDB,1,3,2) —查看SGAM页信息和整体输出页面 DBCC PAGE(TESTDB,1,2,3) —查看GAM页信息及相应列值 DBCC PAGE(TESTDB,1,3,3) —查看SGAM页信息及相应列值 DBCC PAGE(TESTDB,1,2,1) WITH TABLERESULTS —以表格形式查看SGAM页信息及相应列值 DBCC PAGE(TESTDB,1,3,1) WITH TABLERESULTS —以表格形式查看SGAM页信息及相应列值 |
我们可以看到一个完整的页面分为四个部分;BUFFER、PAGE HEADER、DATA和OFFSET TABLE。
让我们首先从GAM页开始看起:
BUFFER部分:
显示给定页面的缓冲信息,是内存中的结构,用于管理页面,该信息仅当该页面处于内存时才有意义。关于这个部分我们知之甚少,基本上无法找到相关材料。
BUF @0x03585CD8 | 每一次清空缓存再次查询,地址都会改变 |
bpage = 0x060B4000 | 每一次清空缓存再次查询,地址都会改变 |
bhash = 0x00000000 | 相对不变 |
bpageno = (1:2) | 当前页面地址 |
bdbid = 8 | sys.databases.database_id |
breferences = 1 | 每一次清空缓存再次查询,地址都会改变 |
bUse1 = 41490 | 每一次清空缓存再次查询,地址都会改变 |
bstat = 0xc00009 | 相对不变 |
blog = 0x59ca2159 | 相对不变 |
bnext = 0x00000000 | 相对不变 |
PAGE HEADER部分:
PAGE HEADER部分显示的是该页面上的所有报头字段的数据
Page @0x060B4000 | 同BUFFER中的bpage地址 |
m_pageId = (1:2) | 数据页号 |
m_headerVersion = 1 | 头文件版本号,一直为1 |
m_type = 8 | 页面类型,8为GAM页面 |
m_typeFlagBits = 0x0 | 数据页和索引页为4,其他页为0 |
m_level = 0 | 该页在索引页(B树)中的级数 |
m_flagBits = 0x200 | 页面标志 |
m_objId (AllocUnitId.idObj) = 99 | 同Metadata: ObjectId |
m_indexId (AllocUnitId.idInd) = 0 | 同Metadata: IndexId |
Metadata: AllocUnitId = 6488064 | 存储单元的ID,sys.allocation_units.allocation_unit_id |
Metadata: PartitionId = 0 | 数据页所在的分区号,sys.partitions.partition_id |
Metadata: IndexId = 0 | 页面的索引号,sys.objects.object_id&sys.indexes.index_id |
Metadata: ObjectId = 99 | 该页面所属的对象的id,sys.objects.object_id |
m_prevPage = (0:0) | 该数据页的前一页面;主要用在数据页、索引页和IAM页 |
m_nextPage = (0:0) | 该数据页的后一页面;主要用在数据页、索引页和IAM页 |
pminlen = 90 | 定长数据所占的字节数 |
m_slotCnt = 2 | 页面中的数据的行数 |
m_freeCnt = 6 | 页面中剩余的空间 |
m_freeData = 8182 | 从第一个字节到最后一个字节的空间字节数 |
m_reservedCnt = 0 | 活动事务释放的字节数 |
m_lsn = (15:216:82) | 日志记录号 |
m_xactReserved = 0 | 最新加入到m_reservedCnt领域的字节数 |
m_xdesId = (0:0) | 添加到m_reservedCnt的最近的事务id |
m_ghostRecCnt = 0 | 幻影数据的行数 |
m_tornBits = 177369273 | 页的校验位或者被由数据库页面保护形式决定分页保护位取代 |
Allocation Status |
|
GAM (1:2) = ALLOCATED | 在GAM页上的分配情况 |
SGAM (1:3) = NOT ALLOCATED | 在SGAM页上的分配情况 |
PFS (1:1) = 0x44 ALLOCATED 100_PCT_FULL | 在PFS页上的分配情况,该页为96%~100%满, |
DIFF (1:6) = CHANGED |
|
ML (1:7) = NOT MIN_LOGGED |
|
PAGE HEADER这部分内容只有通过DBCC PAGE(TESTDB,1,2,2)即整体输出页面才能够展现;通过与上面表格的对照,我们勉强能识别一些相关存储信息;当这部分缺乏官方文档的支持,为了避免无谓的猜测,所以暂时就不做深入探讨了。
5E32C000: 01080000 00020000 00000000 00005a00 †..............Z. |
5E32C010: 00000000 00000200 63000000 0600f61f †........c....... |
5E32C020: 02000000 01000000 0f000000 d8000000 †................ |
5E32C030: 52000000 00000000 00000000 b970920a †R............p.. |
5E32C040: 00000000 00000000 00000000 00000000 †................ |
5E32C050: 00000000 00000000 00000000 00000000 †................ |
DATA 部分
Slot 0, Offset 0x60, Length 94, DumpStyle BYTE |
Record Type = PRIMARY_RECORD Record Attributes = |
Memory Dump @0x5E2AC060 |
00000000: 00005e00 00000000 00000000 00000000 ?..^............. |
00000010: 00000000 00000000 00000000 00000000 ?................ |
00000020: 00000000 00000000 00000000 00000000 ?................ |
00000030: 00000000 00000000 00000000 00000000 ?................ |
00000040: 00000000 00000000 00000000 00000000 ?................ |
00000050: 00000000 00000000 00000000 0000??????.............. |
|
Slot 1, Offset 0xbe, Length 7992, DumpStyle BYTE |
Record Type = PRIMARY_RECORD Record Attributes = |
Memory Dump @0x5E2AC0BE |
00000000: 0000381f 0000c0ff ffffffff ffffffff ?..8............. |
00000010: ffffffff ffffffff ffffffff ffffffff ?................ |
DATA部分一般分为若干插槽号(Slot),如果是数据页或索引页的话,可以理解为一行记录,SQLServer通过文件号+页面号+插槽号用来唯一标识表中的每一条记录。但在GAM页中我们可以把Slot 0理解为GAM页的保留页,共计94个字节。
从第194个字节开始(页面总是从第0个字节开始的),到第196个字节,这三个字节代表已分配的分区的情况。即0000C0。
我们再来看一下DBCC PAGE(TESTDB,1,2,3)的执行结果。
GAM: Header @0x5E2AC064 Slot 0, Offset 96 |
status = 0x0 |
GAM: Extent Alloc Status @0x5E2AC0C2 |
(1:0) - (1:168) = ALLOCATED |
(1:176) - (1:272) = NOT ALLOCATED |
上面显示从第1页到第168页已分配,而第176页到272页未分配,和DBCC PAGE(TESTDB,1,2,2)显示的194个页面似乎有些矛盾,实际上是不矛盾的。如前文所述,GAM对未使用的分区标识为0,而对已分配的分区标识为1
1个分区=64页,因为前128个页面均已分配,所以前两个字节为00 00
从第128个页面起到第175个页面也均已分配,实际上为6个区为0也就是说连续6个bit为0,一个字节为8个bit,最后两个bit为11,所以该字节为0000 0011,在此需要反转一下相关二进制位;反转之后为1100 0000即为C0。
最后让我们用Internals Viewer插件看一下GAM页的全貌吧。
SGAM页面
PAGE: (1:3)
BUFFER: BUF @0x0358A7F4 bpage = 0x062AE000 bhash = 0x00000000 bpageno = (1:3) bdbid = 8 breferences = 3 bUse1 = 14428 bstat = 0xc00009 blog = 0x21212159 bnext = 0x00000000
PAGE HEADER: Page @0x062AE000 m_pageId = (1:3) m_headerVersion = 1 m_type = 9 m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0x200 m_objId (AllocUnitId.idObj)=99 m_indexId (AllocUnitId.idInd)=0 Metadata: AllocUnitId=6488064 Metadata: PartitionId = 0 Metadata: IndexId = 0 Metadata: ObjectId = 99 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:435:5) m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0 m_tornBits = 177043542 Allocation Status GAM (1:2)=ALLOCATED SGAM (1:3)=NOT ALLOCATED PFS(1:1)=0x44 ALLOCATED 100_PCT_FULL DIFF (1:6) = CHANGED ML (1:7) = NOT MIN_LOGGED
DATA: Slot 0, Offset 0x60, Length 94, DumpStyle BYTE Record Type = PRIMARY_RECORD Record Attributes = Memory Dump @0x4F32C060 00000000: 00005e00 00000000 00000000 00000000 ?..^............. 00000010: 00000000 00000000 00000000 00000000 ?................ 00000020: 00000000 00000000 00000000 00000000 ?................ 00000030: 00000000 00000000 00000000 00000000 ?................ 00000040: 00000000 00000000 00000000 00000000 ?................ 00000050: 00000000 00000000 00000000 0000??????..............
Slot 1, Offset 0xbe, Length 7992, DumpStyle BYTE Record Type = PRIMARY_RECORD Record Attributes = Memory Dump @0x4F32C0BE 00000000: 0000381f 20ee2000 00000000 00000000 ?..8. . ......... 00000010: 00000000 00000000 00000000 00000000 ?................ 00001F30: 00000000 00000000 ???????????????????........ |
以下为DBCC PAGE(TESTDB,1,3,3)得到的相关信息,有兴趣的可以和20ee20做一下对比。
(1:0) - (1:32) = NOT ALLOCATED (1:40) - = ALLOCATED (1:48) - (1:64) = NOT ALLOCATED (1:72) - (1:88) = ALLOCATED (1:96) - = NOT ALLOCATED (1:104) - (1:120) = ALLOCATED (1:128) - (1:160) = NOT ALLOCATED (1:168) - = ALLOCATED (1:176) - (1:272) = NOT ALLOCATED |
最后让我们用Internals Viewer插件看一下SGAM页的全貌吧。
总结一下,关于GAM和SGAM页比较困难的地方:
1、 关于GAM和SGAM页中的BUFFER信息基本无法理解,也找不到相关材料。
2、 PAGE HEADER的部分信息和Slot 0中的一部分信息,也无法找到相关材料。
3、 SGAM页中的NOT ALLOCATED实际上是统一类型区或者已使用完的混合类型的区,而ALLOCATED实际上为含有自由页面的混合区。
4、 GAM页中0代表已分配,1代表自由区;和一般的标志位的含义刚好相反。
5、 GAM和SGAM实际上只分配了280个页面,即35个区;显示出来的数据内容虽然很多,但后面的分区信息实际上是不存在的。
6、 GAM和SGAM通过DBCC的printopt为3的属性显示出来的页面分配信息看似是断号的。
7、 GAM和SGAM的区信息的字节是通过二级制反转得到的。
GAM和SGAM页的总的大小为8192个字节;文件头为96个字节,slot 0为94个字节,slot 1的头部的系统信息为4个字节,尾部的系统信息为10个字节,所以有效存储应为7988个字节,63904个区,511230个页;事实上当数据文件超过约4G的时候,我们将能在第511232页、 第511233页分别找到其对应的GAM、SGAM页面。
SQL Server 2008连载之存储结构——PFS结构
PFS(Page Free Space),也叫页面自由空间,该页面用来跟踪一个文件中每一个特定的页面的利用率情况。一个文件中第二个页面(页码1)就是PFS页面,该页面的每个字节都记录了相应页面的分配情况、页面类型、是否IAM页、是否包含删除记录、以及空间利用率信息;PFS能够管理和跟踪8088个页面的使用情况,即接近64M的空间,以后每8088个页面将再出现一次。
让我们首先了解一下PFS的页面管理字节的构造,管理单位为字节,每字节管理一个页面。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 页面是否分配 | 是否混合页面 | 是否IAM页面 | 是否幻影页面 | 空间利用率 |
第0个bit为保留字节,始终为0
第1个bit表示该页面是否已分配,我们知道GAM页用来管理区是否已分配,但一个区包含8个页面,所以用该bit用来准确定位该区的某个页面是否已分配出去了。
第2个bit表示该页面是否混合分区的一个页面。
第3个bit表示该页面是否是一个IAM页面。
第4个bit表示该页面中是否包含幻影或已删除记录,这有助于SQL Server定期清理幻影或已删除记录。
第5~7个页面表示该页面的空间使用率情况。
l 0:表示该页面为空
l 1:表示该页面已使用1~50%
l 2:表示该页面已使用51~80%
l 3:表示该页面已使用81~95%
l 4:表示该页面已使用96~100%
我们可以用dbcc page(testdb,1,1,2)来看一下PFS的页面结构,BUFFER和PAGE HEADER再次就不做详述了,PFS关于页面分配的信息是从第100个字节开始的,最后四个字节为系统保留字节,总计管理8088页。
其中头四页均为44,换算成2进制即0100 0100,即为未分配(大概为保留页的缘故),且为已分配完成的混合区或统一类型区,非IAM页,且无幻影记录,空间利用率96~100%。
4F09C060: 00009c1f 44444444 00004444 60647060 †....DDDD..DD`dp` |
4F09C070: 74706070 60606060 60707060 40404040 †tp`p`````pp`@@@@ |
4F09C080: 40404040 61706070 60606070 60306060 †@@@@ap`p```p`0`` |
4F09C090: 60217024 60706060 60606060 40203020 †`!p$`p``````@ 0 |
4F09C0A0: 20202820 60606060 60606070 60606060 † ( ```````p```` |
4F09C0B0: 70203020 30706070 70607060 70203060 †p 0 0p`pp`p`p 0` |
4F09C0C0: 70203068 70607060 70607060 70607060 †p 0hp`p`p`p`p`p` |
4F09C0D0: 70203060 60602020 60702030 20306070 †p 0``` `p 0 0`p |
4F09C0E0: 60702830 60707070 60606070 60706070 †`p(0`ppp```p`p`p |
4F09C0F0: 60706070 40404020 20202020 60706070 †`p`p@@@ `p`p |
4F09C100: 60706060 64616070 60706070 60706070 †`p``da`p`p`p`p`p |
4F09C110: 60700000 00000000 00000000 00000000 †`p.............. |
4F09C120: 00000000 00000000 00000000 00000000 †................ |
4F09DFF0: 00000000 00000000 00000000 00006000 †..............`. |
最后让我们用Internals Viewer插件看一下PFS页的全貌吧。
第七个页面(页码6)被称为差异变更(DifferentialChanged Map,DCM)页面。它跟踪一个文件中的哪一个区在最新一次完全数据库备份以后被修改过。SQL Server用在增量备份时只对已发生数据变更的分区进行增量备份即可。
第八个页面(页码7)被称为批量更改映射(Bulk ChangedMap,BCM)页面,该页面当文件中的一个区在最小量或批量记日志操作中被使用时用到。就像GAM和SGAM页面,DCM和BCM页面针对它们代表的文件区间中每一个区都有一个比特位相对应。这些页面的常规间距为511 230个页面。
此外关于数据库在进行DML操作如何寻找合适的分区和页面对数据进行处理还是留待后续介绍吧。
SQL Server 2008连载之存储结构——IAM结构
索引分配映射(Index Allocation Map,IAM)页面在4 GB的区间中跟踪被一个分配单元所使用的区。一个分配单元就是一组页面,这些页面属于一个数据表或索引的单个分区。它由下面三种类型页面中的一种组成:含有常规的行内数据的页面、含有大型对象(Large Object,LOB)数据的页面和含有行溢出数据的页面。其实SQL Server的数据页面类型与Oracle的段的概念有些类似,一个对象包含若干段,而一个段只能属于一个对象。
假如一张在四个分区上的含有所有三种类型的数据(行内数据、LOB数据和行溢出数据)的表将会有至少12个IAM页面。单张IAM页面也是仅仅覆盖单个文件的4GB区间,所以如果分区跨越多个文件,那么就会有多个IAM页面,同时如果文件大小超过4GB,并且分区使用了一个4 GB区间以外的数据页,那么也将会有额外的IAM数据页。
一个IAM数据页包含一个页头(IAM页头),该页头包含有8个页面指针槽,还有一组比特位用来将一个范围内的区映射到一个文件,这个文件并不必一定就是IAM页面所在的那个文件。页头包含有在IAM映射范围内的第一个区的地址。8个页面指针槽可能包含指向某些属于相关对象页面的指针,这些对象被包含在混合类型的区中,对一个对象来说,只有第一个IAM页面含有这些指针的值。一旦一个对象占用的页面超过8个,它所有的区都会是统一类型的区——这意味着一个对象决不会需要超过8个指针来指向处于混合类型区中的页面。如果一张表中的数据行已被删除,该表实际上可以使用的指针数不到8个。比特位映射中的每一个比特位代表了该范围内的一个区,而不论该区是否被分配给了拥有该IAM的对象。如果一个比特位是打开的,那么在此范围内相关的区就是被分配给拥有
IAM的对象的;如果一个比特位是关闭的,那么此范围内相关的区没有被分配给拥有该IAM的对象。
IAM页面在需要的时候被分配给每一个对象,并且位于数据库中的随机位置。每一个IAM页面覆盖的可能范围大约是512 000个页面。
看概念总归是比较枯燥的,我们可以构建一个具体的例子。
在构建例子之前我们首先需要创建一个把地址转换为具体页码的函数。
CREATE FUNCTION [dbo].f_get_page(@page_num BINARY(6)) RETURNS VARCHAR(11) AS BEGIN RETURN(CONVERT(VARCHAR(2),(CONVERT(INT,SUBSTRING(@page_num,6,1))*POWER(2,8))+ (CONVERT(INT,SUBSTRING(@page_num,5,1))))+':'+ CONVERT(VARCHAR(11), (CONVERT(INT,SUBSTRING(@page_num,4,1))*POWER(2,24))+ (CONVERT(INT,SUBSTRING(@page_num,3,1))*POWER(2,16))+ (CONVERT(INT,SUBSTRING(@page_num,2,1))*POWER(2,8))+ (CONVERT(INT,SUBSTRING(@page_num,1,1))))) END --根据master.sys.objects构建一张叫testIAM的数据表 SELECT * INTO testIAM FROM master.sys.objects --然后我们根据之前所知晓的信息,获取testIAM对象的IAM地址,并根据f_get_page函数将地址转换为相应的页面 SELECT total_pages,used_pages,data_pages, first_page,root_page,first_iam_page, testdb.dbo.f_get_page(first_page) first_page_address, testdb.dbo.f_get_page(root_page) root_address, testdb.dbo.f_get_page(first_iam_page) IAM_address FROM sys.system_internals_allocation_units WHERE container_id IN (SELECT partition_id FROM sys.partitions WHERE object_id in (SELECT object_id FROM sys.objects WHERE name IN ('testIAM'))) dbcc page(testdb,1,80,3) |
从dbcc page(testdb,1,80,3)可以得到以下信息
IAM: Header @0x5E20C064 Slot 0, Offset 96 sequenceNumber = 0 status = 0x0 objectId = 0 indexId = 0 page_count = 0 start_pg = (1:0)
IAM: Single Page Allocations @0x5E20C08E Slot 0 = (1:77) Slot 1 = (1:89) Slot 2 = (0:0) 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 @0x5E20C0C2 (1:0) - (1:272) = NOT ALLOCATED |
因为master.sys.objects的对象只有49条数据,所以只分配了2个页面,根据前文所述,分配8个页面(包括8)以内的对象,SQL Server将以把该对象的数据分配到混合类型的分区上,如上SQL Server给予testIAM表只分配了第一个文件的第77和第89个页面,而并没有分配同一类型的分区的页面。
假如我们master.sys.objects的数据反复插入testIAM表,让我们观察一下相应的页面分配情况。
declare @maxtime int set @maxtime=0 while @maxtime<20 begin insert into testiam select * from sys.objects set @maxtime=@maxtime+1 end select * from testiam --我们首先还是运行以下system_internals_allcation_units系统表 SELECT total_pages,used_pages,data_pages, first_page,root_page,first_iam_page, testdb.dbo.f_get_page(first_page) first_page_address, testdb.dbo.f_get_page(root_page) root_address, testdb.dbo.f_get_page(first_iam_page) IAM_address FROM sys.system_internals_allocation_units WHERE container_id IN (SELECT partition_id FROM sys.partitions WHERE object_id in (SELECT object_id FROM sys.objects WHERE name IN ('testIAM'))) --通过上面的结果,我们可以观察到这次SQL Server共分配了17个页面,其中使用了15个页面,而数据页面只有14个,这是为什么呢? --接着我们再次运行dbcc page命令 dbcc page(testdb,1,80,3) |
结果如下
IAM: Single Page Allocations @0x4F1FC08E Slot 0 = (1:77) Slot 1 = (1:89) Slot 2 = (1:45) Slot 3 = (1:78) Slot 4 = (1:90) Slot 5 = (1:109) Slot 6 = (1:114) Slot 7 = (1:120) IAM: Extent Alloc Status Slot 1 @0x4F1FC0C2 (1:0) - (1:168) = NOT ALLOCATED (1:176) - = ALLOCATED (1:184) - (1:272) = NOT ALLOCATED |
从上述我们可知,slot 0到slot 7一共分配了8个混合类型区的页面,由于已经超过8页,所以SQL Server再次分配空间时,就会把同一类型的区分配给该对象,一个区包括8个页面,所以SQL Server为testIAM表共分配了16个页面,数据页面14个,已使用的页面除了数据页面还包括该表的一个IAM管理页面。
还是通过Internals Viewer插件让我们看一下IAM页的情况吧
比较有意思的是SQL Server 2008为什么不从一开始就为每一个对象分配同一类型的页面,仅仅是为了节约空间?还是为了与之前版本的兼容性?就不得而知了。
SQL Server 2008连载之存储结构——DCM&BCM
如前所述页码6被称为差异变更(DifferentialChanged Map,DCM)页面。它跟踪一个文件中的哪一个区在最新一次完全数据库备份以后被修改过。这样SQL Server用在增量备份时只对已发生数据变更的分区进行增量备份即可。
那么首先让我们执行一下dbcc page(testDB,1,6,2)命令,可以看出前96字节为文件头,接下来的96个字节为保留页面,从第195个字节才开始记录区是否已做变更。由于是新库,数据对象并不多;ffff 7f,这三个字节记录了需要进行下次备份需要进行增量备份的信息。
DATA: |
Memory Dump @0x4F1FC000 |
4F1FC000: 01100000 00000000 00000000 00005a00 †..............Z. |
4F1FC010: 00000000 00000200 63000000 0600f61f †........c....... |
4F1FC020: 06000000 01000000 16000000 ce000000 †................ |
4F1FC030: 2a000000 00000000 00000000 997410aa †*............t.. |
4F1FC040: 01000000 00000000 00000000 00000000 †................ |
4F1FC050: 00000000 00000000 00000000 00000000 †................ |
4F1FC060: 00005e00 00000000 00000000 00000000 †..^............. |
4F1FC070: 00000000 00000000 00000000 00000000 †................ |
4F1FC080: 00000000 00000000 00000000 00000000 †................ |
4F1FC090: 00000000 00000000 00000000 00000000 †................ |
4F1FC0A0: 00000000 00000000 00000000 00000000 †................ |
4F1FC0B0: 00000000 00000000 00000000 00000000 †................ |
4F1FC0C0: 381fffff 7f000000 00000000 00000000 †8............... |
4F1FC0D0: 00000000 00000000 00000000 00000000 †................ |
4F1FC1B0: 00000000 00000000 00000000 00004000 †..............@. |
4F1FC3B0: 00000000 00000000 04000000 00000000 †................ |
4F1FC5B0: 00400000 00000000 00000000 00000000 †.@.............. |
让我们换个视图来看一下,即执行dbcc page(testDB,1,6,3),这样可以清楚地看到只有第0页到第183页是CHANGED状态,下次备份需要备份这些页面。
DIFF_MAP: Header @0x4F1FC064 Slot 0, Offset 96 status = 0x0 DIFF_MAP: Extent Alloc Status @0x4F1FC0C2 (1:0) - (1:176) = CHANGED (1:184) - (1:272) = NOT CHANGED |
接下来当我们执行一次testDB库全备后,再次用dbccpage(testDB,1,6,3)观察一下变化。
DIFF_MAP: Extent Alloc Status @0x5E52C0C2 (1:0) - (1:16) = CHANGED (1:24) - (1:56) = NOT CHANGED (1:64) - = CHANGED (1:72) - = NOT CHANGED (1:80) - = CHANGED (1:88) - (1:272) = NOT CHANGED |
就会发现除了一下系统保留页面,基本上都变更为NOT CHANGED状态,记住DCM页面记录的是区变更信息,并且系统保留页面是一定要备份的。
BCM页
页码7被称为批量更改映射(Bulk ChangedMap,BCM)页面,只有在数据库处于BULK_LOGGED模式,并且没有执行任何bulk批量操作时,才被使用到,因为BULK_LOGGED模式时数据库日志记录了包含数据库所有改变的完整顺序记录,所以我们能够将数据库还原到任一时间点。
大容量日志恢复模式是一种特殊用途的恢复模式,只应偶尔用于提高某些大规模大容量操作(如大量数据的大容量导入)的性能
与完整恢复模式(完全记录所有事务)相比,大容量日志恢复模式只对大容量操作进行最小记录(尽管会完全记录其他事务)。大容量日志恢复模式保护大容量操作不受媒体故障的危害,提供最佳性能并占用最小日志空间。
但是,大容量日志恢复模式会增加这些大容量复制操作丢失数据的风险,因为大容量日志操作阻止再次捕获对每个事务逐一所做的更改。如果日志备份包含大容量日志操作,则无法还原到该日志备份中的时点,而只能还原整个日志备份。
为跟踪数据页,日志备份操作依赖于位图页的大容量更改,位图页针对每个区包含一位。对于自上次日志备份后由大容量日志操作所更新的每个区,在位图中将每个位都设置为 1。
因为BCM页的应用场景比较单一,在此不对BCM页做相关详述。