(3.6)sql server存储引擎--文件与数据页及数据行的结构

官网:https://docs.microsoft.com/zh-cn/sql/relational-databases/pages-and-extents-architecture-guide?view=sql-server-ver15

一. 文件

(1)主数据文件.mdf初始大小至少为3MB(在sql2012/2008以后至少需要5M),次要数据文件.ndf初始大小,同日志文件一样至少为512KB(在sql2012/2008以后至少需要1M);

(2)SQL SERVER在逻辑上用文件组将文件分批管理(类似ORACLE的TABLESPACE),一个文件组可以包含多个文件,插入数据时,同一个文件组内的所有文件等比例增长(ORACLE的一个TABLESPACE中逐个使用多个文件),例如:文件组中有两个文件,初始大小分别为100M和200M,此时插入3M数据,file1新增(100/300)*3M=1M,file2新增(200/300)*3M=2M,如下图:

  

 

 

(3)页(page),SQL SERVER中的数据文件由8K大小的数据页组成,每个数据文件中的页从0开始编号,页大小不可以自定义(ORACLE可自定义),且每个页只可以属于一个数据对象;

(4)区(extent),或者叫扩展,8个物理上连续的页为一个扩展,即64K,扩展的存在是为了避免不停地分配8K的页面,提高页面分配的效率。SQL SERVER有两种类型的区,如下图:

  (4.1)混合区:为了节约空间,将少量数据的表或索引存放在混合区中,当表或索引的数据增长到8页时,再使用统一区来存放,一个混合区有8个页,每个页可以属于不同的数据对象,即每个混合区最多为8个数据对象共享;

  (4.2)统一区:由单个数据对象所有,如果对表中现有数据创建索引,且索引的大小超过8页,则索引将全部使用统一区,没有混合区的分配过程。

   

 

 

二. 页

2.1、非数据页

(1)文件头(FILE HEADER),每个数据文件的第1页,页号为0,该页主要包括当前文件的属性描述,比如:文件组ID、文件ID、文件当前大小、文件最大/最小值、文件增量、一系列的LSN等;

(2)页面空闲空间(PFS),每个数据文件的第2页,页号为1,该页记录当前数据文件每个数据页的空间状态:该页是为空、已满 1% 到 50%、已满 51% 到 80%、已满 81% 到 95% 还是已满 96% 到 100%。PFS页内用1个字节来描述1个数据页的分配及空间状态,每个PFS页约有可用空间8088个字节,即数据文件内约每64M的空间会出现一个PFS页。PFS页描述数据页空间状态如下图:

  

 

 

(3)全局分配映射(GAM),每个数据文件的第3页,页号为2,该页记录当前数据文件每个区的分配状态,0为已使用(作为混合区或统一区,已被分配),1为未使用(自由区,未被分配);

  (3.1)结合PFS和IAM页,如果数据对象没有可用空间时,且数据大小已超过8页,GAM为数据对象分配一个统一区;若数据大小尚未超过8页,则GAM结合SGAM为其寻找或者分配一个混合区;

  (3.2)GAM页内用1位来描述1个区的分配状态,每个GAM页约有可用空间8000个字节,即数据文件内约每4G的空间会出现一个GAM页;

(4)共享分配映射(SGAM),每个数据文件的第4页,页号为3,该页记录当前数据文件哪些区被用作混合区,1为含有自由页面的混合区,0为自由区或已满的混合区;

  (4.1)当数据对象需要一个含有自由页面的混合区时,SGAM用来辅助GAM寻找或分配一个混合区;

  (4.2)SGAM页内用1位来描述1个区的分配状态,每个SGAM页约有可用空间8000个字节,即数据文件内约每4G的空间会出现一个SGAM页;

(5)索引分配映射(IAM),该页跟踪数据文件中的页属于哪一个数据对象,IAM页头有8个页面指针,指向数据对象在混合区中的数据页(如果混合区中数据被删除可能少于8个指针),IAM页内比特位为1表示该区属于自己所属的数据对象,比特位为0表示该区不属于自己所属的数据对象;

  (5.1)每个数据对象的每个分配单元拥有一个IAM页,IAM同GAM、SGAM一样可以管理约4G的空间,如果分配单元包含多个文件,或者文件大小超过4G,则需要另外的IAM页来管理,IAM页间通过双向链表连接;

  (5.2)可以通过sysindexes或sys.system_internals_allocation_units系统目录得到first_IAM页面的位置,IAM页在数据文件中的位置是随机的,可能IAM页所在文件并不是所管理的那个文件;

      

  (5.3)对于堆数据插入而言,通过IAM页和PFS页找到自己有剩余空间的页,直接插入数据即可,但索引数据插入的位置是由索引键的顺序决定的;

(6)差异更改映射(DCM),每个数据文件的第7页,页号为6(页号4,5为保留页),该页跟踪当前数据文件中,自上次全备份后被修改的区,以提高差异备份的效率,1为被修改过,0为未被修改;

  (6.1)DCM页内用1位来描述1个区的分配状态,每个DCM页约有可用空间8000个字节,即数据文件内约每4G的空间会出现一个DCM页;

(7)大批量更改映射(BCM),每个数据文件的第8页,页号为7,该页跟踪当前数据文件中,自上次日志备份后被大批量操作修改的区, 1为被修改过,0为未被修改;

  (7.1)大批量恢复模型只记载大批量操作的最小日志记录(不记录明细,只记载动作),所以比在全恢复模型下执行速度要快(因为省去写大量日志的成本),但为了保证数据库的可恢复,在进行日志备份时,仍然会使用BCM页将大批量操作所修改的区备份出来,所以此时会出现日志很小,但日志备份很大的情况;

  (7.2)BCM页只有在大批量恢复模型下才有用,因为简单恢复模型下不记载任何大批量操作的日志,且日志自动截断,全恢复模型下有详细的日志纪录;

  (7.3)BCM页内用1位来描述1个区的分配状态,每个BCM页约有可用空间8000个字节,即数据文件内约每4G的空间会出现一个BCM页;

 

2.2、数据页

2.2.1、DATA

(0)数据页,包括页头、数据行、行偏移矩阵三部分内容,如下图:

  

 

 

(0.1)页头固定大小为96B,包括页号、所属对象、LSN等页面信息;

(0.2)数据行累计不超过8060B,那么单行数据最长为8060B,由于数据行还存在一些行开销,所以建表时,数据类型的最大长度不允许超过8000B(LOB类型除外),行开销在下面会有介绍;

(0.3)行偏移矩阵以2B为一个指针,标识每一个数据行的起点位置。哪怕是索引页,数据行在页内的物理顺序也并不一定是有序的,数据读取时按行偏移矩阵的顺序读出(从slot0起),从逻辑上保证了数据行在页内的顺序,可能slot0对应的数据行是在页面内物理上的第N行(N<>1);

(1)行内数据(IN_ROW_DATA)

  单行未超过8060B的数据行,或者单行超过8060B但存储在当前页的数据,称为行内数据;

(2)行溢出数据(ROW_OVERFLOW_DATA)

  在SQL SERVER 2005及以后的版本中,如果表中定义了变长数据类型,允许单行数据长度突破8060B,超过的部分即为行溢出数据,如果变长列被更新后缩短,可能会被移回行内数据页(通常减少1000字节以上时,SQL SERVER才会有检查是否可移回);

  (2.1)行溢出数据为超过8060B数据行的一部分列,在行内数据页上有24B的指针指向行溢出数据页;

  (2.2)只会将变长数据类型的行溢出数据移出行内数据页,因为定长数据类型在定义表时就不允许累计长度超过8000B,所以行内数据页足够存放定长数据列;

  (2.3)移出行内数据页的变长列,必然是整列数据,不会将变长列数据拆开存放,一个新的数据页也足够存放最大长度为8000B的变长列;

  (2.4)如果变长列使用了MAX分类符,如:varchar(MAX),则数据库自动根据varchar(MAX)的数据长度选择不同的数据页,如果MAX<=8000,则使用行溢出数据页;如果MAX>8000则使用大对象数据页;

(3)大对象数据(LOB_DATA)

  存放如:TEXT/IMAGE/XML/varchar(MAX)等最大长度可超过8000B的数据类型的数据;

  (3.1)大对象数据也是通过8k的数据页来存储数据,在行内数据页中包含一个16字节的指针指向大对象数据的根页,大对象数据通过B-树结构来组织多个数据页;

  (3.2)可以通过打开text in row选项将大对象数据存储在行内数据页,当大对象数据被更新超过500B时,则会从行内数据页将大对象数据移出,这是个日志操作,因此移动操作比较耗时,所以不建议开启该选项;

(4)数据行

每个数据行,除了每个列的数据之外,还包括状态位、定长列偏移量、总列数、NULL位图、变长列数、列偏移矩阵,这些即为行开销。

创建全定长列的表,数据行如下图:

if object_id ('test_col') is not null
    drop table test_col;
GO
create table test_col
(
col1 char(1),
col2 char(2)
)
GO
insert into test_col
values('A','B')

  

 

 

(4.1)状态位A占1B标识列的类型、是否有变长列等信息,状态B占1B未启用;

(4.2)定长列偏移量占2B,标识定长列的截止位置,这里为1+1+2+1+2=7B,每个定长列的起始位置,可以在无文档记载的系统视图sys.system_internals_partition_columns中查到(leaf_offset字段),col1、col2为定长数据所在;

(4.3)在包含可为NULL的列时(定长列为NULL时列值为0,而变长列不存储列值),这时就需要总列数(占2B)和NULL位图了,比如:有4列,NULL位图为11110100(由低位向高位对应),则表示第3列为NULL,那么第1、2、4列返回数据,第3列返回NULL。上图中两列均可为NULL(11111100)但都有值,所以不返回NULL,NULL位图的长度随着表中列数的多少在变化,以BYTE为单位进行增长,最小为1B即8位,可以标识8列;

(4.4)全定长的数据行中,不包括变长列数、列偏移矩阵的行开销;

创建包含变长列的表,数据行如下图:

if object_id ('test_col2') is not null
    drop table test_col2;
GO
create table test_col2
(
col1 char(1),
col2 varchar(2)
)
GO
insert into test_col2
values('A','B')

  

(4.5)此时定长列偏移量为1+1+2+1=5B,因为只一个长度为1B的定长列,另外,总列数仍然是2,NULL位图也相同;

(4.6)变长列数占2B,表示表中有几个变长列,它决定了后面的列偏移矩阵中要放几个元素,列偏移矩阵标识表中每个变长列的截止位置,col2为变长数据所在;

创建全变长列的表,数据行如下图:

 

if object_id ('test_col3') is not null
    drop table test_col3;
GO
create table test_col3
(
col1 varchar(1),
col2 varchar(2),
col3 varchar(3)
)
GO
insert into test_col3
values(NULL,'','B')

  

 

 

(4.7)此时定长列偏移量为1+1+2=4B,因为没有定长列,另外,总列数为3,NULL位图为11111001,表示第1列为NULL,第2列与第1列偏移量一样,表示没有值,但又不为NULL,即空字符串,变长列数为3,列偏移矩阵中共有3个元素;

(4.8)当表中的列被删除时,列在数据行中的占用的空间并不会立即被回收,除非立即重建索引,或者当空间不足时数据库才考虑回收;

(4.9)当表中的列被修改时:

  (a)从NULL到NOT NULL,数据类型长度缩短都会带来全表的该列数据检查,对于大表将非常耗时;

  (b)对于修改数据类型长度,并没有真正替换原有列,只是新增了一列,根据列偏移量读取数据时,原有列不会被读到而已,并且对于定长列,只是限制了新插入数据的值范围,列存储空间依旧使用改变前的数据类型的长度,如下图:

alter table test_col alter column col2 char(1)
--以下语句将失败
insert into test_col
values('A','BB')

 

 

 

此时定长列偏移量仍然为1+1+2+1+2=7B,总列数仍然为2,新增的列位于原有列之前;

 

  (c)修改变长列数据类型长度后,列偏移矩阵会向后移动,如下图:

alter table test_col3 alter column col3 varchar(1)

 

 

此时变长列数为4,原来的第3列从0x11到0x12,长度为1,第4列为原有列,并且新的列值将会使用新的数据类型长度;

 

(4.10)当表中新增列时,无法指定新列的逻辑顺序(列号),只能排在后面,除非重建表,在某些图形工具中将新列插入到某个位置,事实上就是在重建表;新增列的物理顺序放在数据行的相应数据区域,如下图:

alter table test_col3 add col_new char(1) not null default 'a'

 

 

 

定长列col_new被放在定长数据区域,从列偏移矩阵可以看出,后面的变长数据相应的向后移动了1B;

转自:http://blog.51cto.com/qianzhang/1217431

 

 

1 数据页的类型

   不同类型的数据,存储在不同类型的页面里,大致可以分为3个方向:存储实际数据的、存储管理数据页数据的及存储备份相关的数据,这3个反向,又各有不同的page type。详见下表。
 
Id User_for Page Type Page Name Description
1 实际数据 数据页 Data Page 堆表或者聚集索引的叶子节点
2 索引页 Index Page 聚集索引的分支节点或者非聚集索引
3 LOB LOB 用来存放大型对象数据类型:text , image ,varchar(max) , varbinary(max)等
4 行溢出页 Row Overflow Page 只能存储单一text或者image列数据块
5 管理数据页数据 GAM页 Global Allocation Map 管理统一区的位图
6 SGAM页 Shared Global Allocation Map 管理混合区的位图
7 IAM页 Index Allocation Map 分配单元分配到的区
8 PFS页 Page Free Space 可用空间
9 备份相关的数据 DCM页 Differential Changed Map 自最后一条backup database 语句之后更改的区的信息
10 BCM页 Bulk Changed Map 自最后一条backup log语句之后的大容量操作所修改的区的信息
 
    针对备份相关数据的页面类型,这里不做详细描述。存储实际数据的页面类型, 后面会有几篇博文详细描述。本文重点讲解下 存储 管理数据页数据 的页面类型:GAM,SGAM,IAM,PFS 。这块,对tempdb的性能优化会有比较大的帮助。
    每一个数据文件的开头都分布GAM,SGAM,PFS这些页面,它们记录了这个数据库文件中哪些页面已经被使用,哪些页面还没有使用。

 

1.1 PFS

      PFS页,用来跟踪页分配级别,存储当前数据文件里所有页分配及可用空间的信息,每一个数据文件的第2个数据页都是PFS,页号为1 。该页面中,每一个字节描述后面每一个数据页是否还有空间可以写记录,也就是一个PFS页是8k,约有8k个字节可以描述后续每个页面的使用情况,也就是一个PFS页,可以描述8k个数据页的使用情况,这就意味着单个PFS页能够存储约64M数据页的可用空间情况。所以,大约每隔64Mb,就会有一个新的PFS页。
    每个字节描述一个数据页的使用情况,一个字节有8个bits,分别第0-7位,用途如下:
  • bit 0-2位,描述该页还有多少空闲空间
    • 0x00 is empty
    • 0x01 is 1 to 50% full
    • 0x02 is 51 to 80% full
    • 0x03 is 81 to 95% full
    • 0x04 is 96 to 100% full
  • bit 3 (0x08): 该数据页是否存在鬼影记录(ghost records:http://www.cnblogs.com/lyhabc/archive/2013/06/16/3138214.html)?
  • bit 4 (0x10): 是否是IAM页?
  • bit 5 (0x20): 是否是混合页?
  • bit 6 (0x40): 是否已分配使用?
  • Bit 7 保留,未使用,无实际含义

1.2 GAM & SGAM & IAM

    SQL SERVER的区分为两种类型:混合区(uniform extent)跟统一区(mixed extent)。
    混合区,指区内连续的8个数据页,分别分配给不同的数据库对象存储使用,这有利于小表在使用的过程中,没有一下子就占据了一个区,而是先从混合区使用,合理分配空间;统一区,指的是区内连续8个页都是用来存储同一个数据对象的,当一个表格分配了8个混合区页后,会开始分配使用统一区,避免数据分散存储在各个不连续的数据页中。

1.2.1 GAM

  GAM页,用来跟踪区的分配情况,描述每区是否被分配,每个区用1bit标识其分配情况。一个GAM页8k,一共有8k*8 bit,也就是差不多可以标识 64000个区,约 8k*8bit*(8*8k)=4G 空间,所以,大概每隔 4G空间左右,就有一个GAM页来标识下一个4G的空间分配情况。
  • Bit=1,标识当前的区是空闲的,可以用来分配;
  • Bit=0,标识当前的区以及被数据使用。
    SQL Server通过读取GAM页找到可用空间,并把这个区或者区内的某一个页分配给一个对象。一个范围仅存储一个比特(而不是像PFS页,PFS是一个页一个字节),意味着单个GAM页能够追踪更多空间,在一个数据文件中,你可以在大约4GB间隔的空间找到一个新GAM页。然而,数据文件中的第一个GAM页的页码总是2,因此“2:1:2”就表示tempdb中的第一个GAM页。

1.2.2 SGAM

    SGAM页,用来跟踪区的分配情况,描述哪些区是混合区并且至少有一个空闲的数据页。1bit描述一个区,1表示该区是混合区且至少有一个空闲的数据页,代表该区可以分配给需要使用混合区的对象。
    SQL Server通过读取SGAM页来找到与可用空间混合的范围来把空间分配给小对象。单个SGAM页能够追踪4GB的空间,因此,你可以在4GB的间隔中找到它们,就像GAM页一样。在数据文件中,第一个SGAM页是页3,所以“2:1:3”就表示tempdb的第一个SGAM页。
    那么,这两种类型的数据页是如何协助存储引擎对区进行管理的呢?
  • 当存储引擎分配一个统一区时,在GAM页中寻找标记为1的页面,把标记修改为0,SGAM页中的标记位不做变动,保持为0;
  • 当存储引擎分配一个混合区时,在GAM页中寻找标记为1的页面,把标记修改为0,SGAM页中的标记位从0修改1;
  • 当存储引擎寻找一个有空闲页的混合区是,直接在SGAM页中查找标记位1对应的数据页;如果没有找到,则会重新分配一个混合区。

1.2.3 IAM

    区的分配,使用GAM及SGAM管理;页的使用情况,采用PFS页管理。那么,每一个数据库对象在各个页里或者区里的存储情况,由谁协助管理呢?答案是 IAM页。
    一个表格中,IAM页用来描述数据的分布情况,基于分配单元来描述。根据实际数据的存储情况,分为以下3种分配单元:
  • IN_ROW_DATA
    • 存储堆或索引分区,即heap和B-tree。
  • LOB_DATA
    • 存储大型对象 (LOB) 数据类型,例如 xml、varbinary(max) 和 varchar(max)。
  • ROW_OVERFLOW_DATA
    • 存储超过 8,060 字节行大小限制的 varchar、nvarchar、varbinary 或 sql_variant 列中存储的可变长度数据。

    每个有数据的表格,至少有一个 IAM页来管理 IN_ROW_DATA的存储情况,如果表格里有LOB_DATA,则会多一个IAM页来管理LOB_DATA,ROW_OVERFLOW_DATA也是一样。

2 数据页结构

    数据页有4个部分:页头、行记录、空闲空间及行偏移量。详见下图:
    

2.1 页头

占用96字节,存储跟该页面相关的系统数据。

    页头的内容如下:

2.2 行记录

  • 存储数据行记录以及索引数据
  • 行记录也可以在独立页面上存储,比如行溢出数据即LOB数据

2.3 空闲空间

  • 除去页头,行记录,以及偏移量剩下的空间,提供给行记录及行偏移量使用

2.4 行偏移量

  • 行偏移是一个个小块组成的,每个小块2个字节,表示数据行从第几个字节后开始记录,也就是距离页头多少偏移量开始记录
  • 存储方式是从有往左存储,用槽位来描述,slot 0 ,slot 1 ....
  • 行偏移量记录的内容是什么呢?该行记录从哪个字节开始,一般情况下,slot 1 从第96个字节后开始
  • 常说的聚集索引存储顺序是物理排序,指的不是行记录物理排序,而是行偏移量物理排序,数据页中,行记录都是顺序往后添加的,通过修改行偏移量来达到聚集索引的顺序查找

3 查询数据页存储格式的途径

     查看数据页存储方式这里简单介绍两种方式:dbcc page查看以及dbcc ind查看,这两个指令都是非公开的指令,所以msdn上找不到相应的使用说明。不过,还是可以通过 技术内幕的相关资料来查阅分析。
这两个指令在打开跟踪标记2588后可以查看其参数说明,打开跟踪标记3604后,可以把指令执行结果放回到客户端而不是记录在错误日志中。

3.1 dbcc ind

3.1.1 语法说明

    查看ind的参数说明,打开跟踪标记2588,help查询。
 
复制代码
DBCC TRACEON(2588)
DBCC HELP('ind')
 
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
dbcc IND ( { 'dbname' | dbid }, { 'objname' | objid }, { nonclustered indid | 1 | 0 | -1 | -2 } [, partition_number] )
 
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
复制代码
 
  输出的格式有4种方式,不同方式,输出不一样。 
  • -2:返回所有IAM页,基于管理行内数据页,行溢出数据页及大对象数据页的IAM页
  • -1:返回所有IAM页及数据页。
  • 0:返回管理行内数据页的IAM页,行内数据页
  • 1:返回聚集索引的数据页信息及IAM页信息(同-1)
  • 2:返回第1个非聚集索引的数据页信息及IAM页信息
  • 3:返回第2个非聚集索引的数据页信息及IAM页信息
  • ...
  • n:返回第(n-1)个非聚集索引的数据页信息及IAM页信息(n>1)

 

3.1.2 测试案例

    新建表格,tbpage_c,包含大数据对象及行溢出情况。INSERT一行数据,然后分析。数据库中,每行数据默认存储在同一个数据页,如果一个数据页存储不了一行数据,则会出现行溢出情况,具体可自行了解行溢出,所以在这个表格里边,设置两个数据列占用空间>8k,具体见表SQL如下,造数据的SQL如下。
 
复制代码
create table tbpage_c(id int identity(1,1) not null primary key ,namea varchar(6000),nameb varchar(3000),descriptions text)
 
#name_a INSERT 6000个字符,name_b INSERT 3000个字符,descriptions INSERT 100个字符
 
INSERT INTO tbpage_c(NAMEA,nameb,descriptions)
select
      substring(stuff((select name+',' from master.dbo.spt_values for xml path('')),1,1,''),1,6000) ,
      substring(stuff((select name+',' from master.dbo.spt_values for xml path('')),1,1,''),1,3000) ,
      substring(stuff((select name+',' from master.dbo.spt_values for xml path('')),1,1,''),1,100)
复制代码
 

dbcc ind('dbpage','tbpage_c',-2)

选项为-2,显示表格的所有IAM页面。由于表格存在行溢出及大对象列,所以会有其相对应的IAM页面,故可以看到有3个IAM,分别为 In-row data ,Row-overflow data ,LOB data。

 
 
dbcc ind('dbpage','tbpage_c',-1)
选项为-1,返回所有IAM页及数据页。
  • 数据页号310,309属于 In-row data 类型。309记录实际数据,310记录In-row data实际数据页的分布情况。
  • 数据页号307,308属于 Row-overflow data 类型。307记录实际数据,308记录 Row-overflow data 实际数据页的分布情况。
  • 数据页号305,306属于 LOB data 类型。305记录实际数据,306记录 LOB data  实际数据页的分布情况。
 

 

dbcc ind('dbpage','tbpage_c',0)
选项为0,返回管理行内数据页的IAM页,行内数据页,故此处仅返回 In-row data 相关数据页。
 

 

dbcc ind('dbpage','tbpage_c',1)
选项为1,返回聚集索引涉及到所有IAM页及数据页。
 

3.2 dbcc page

3.2.1 语法说明

    查看page的参数说明,打开跟踪标记2588,help查询。
 
复制代码
DBCC TRACEON(2588)
DBCC HELP('PAGE')
 
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系。
dbcc PAGE ( {'dbname' | dbid}, filenum, pagenum [, printopt={0|1|2|3} ])
 
DBCC 执行完毕。如果 DBCC 输出了错误信息,请与系统管理员联系
复制代码
    输出的格式有4种方式,不同方式,输出不一样。
  • 0:输出可读形式的数据页页头数据
  • 1:输出可读形式的数据页页头数据,并且还有槽位对应记录的十六进制内容
  • 2:输出可读形式的数据页页头数据,输出整个数据页页头的十六进制数据,整一页的内容都显示,包括未使用的空间。
  • 3:输出可读形式的数据页页头数据,并且包括记录中每个字段的可读形式,行溢出数据也会显示数据内容,但是大对象则不显示内容,而是说明其存储位置!所以选项3,也是输出内容最全面的。

3.2.2 测试案例

    采用3.1.2的表格,分析其 IN_ROW DATA的数据页面,page_id=309 。
    本次测试,没有采用 with tableresults分析,如果dbcc page('dbpage',1,309,0) with tableresults,结果则是已表格形式返回,可以提供后期管理分析用。
    选项为0,输出可读格式的数据页 页头数据。
 
复制代码
DBCC TRACEON(3604)
dbcc page('dbpage',1,309,0) ---------------------------------------------------------------------------------------------------------- PAGE: (1:309) BUFFER: BUF @0x000000027C0827C0 bpage = 0x000000026FA86000 bhash = 0x0000000000000000 bpageno = (1:309) bdbid = 10 breferences = 0 bcputicks = 0 bsampleCount = 0 bUse1 = 46781 bstat = 0xb blog = 0x212121cc bnext = 0x0000000000000000 PAGE HEADER: Page @0x000000026FA86000 m_pageId = (1:309) m_headerVersion = 1 m_type = 1 /* m_pageId 当前页面号码;m_headerVersion 版本号,始终为1;m_type 页面数据类型,1为数据页面,10为IAM页面等,具体参考pagetype */ m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000 /* m_typeFlagBits 数据页和索引页为4,其他页为0 m_level 该页在索引页(B树)中的级数,0表示为叶子节点 m_flagBits 页面标志 */ m_objId (AllocUnitId.idObj) = 35 m_indexId (AllocUnitId.idInd) = 256 /* m_indexId (AllocUnitId.idInd) 索引ID,0 代表堆, 1 代表聚集索引, 2-250 代表非聚集索引 大于250就是text或image字段 */ Metadata: AllocUnitId = 72057594040221696 Metadata: PartitionId = 72057594038976512 Metadata: IndexId = 1 Metadata: ObjectId = 341576255 m_prevPage = (0:0) m_nextPage = (0:0) /* Metadata: AllocUnitId 存储单元的ID,sys.allocation_units.allocation_unit_id Metadata: PartitionId 数据页所在的分区号,sys.partitions.partition_id Metadata: ObjectId 该页面所属的对象的id,sys.objects.object_id Metadata: IndexId sys.objects.object_id&sys.indexes.index_id m_prevPage 该数据页的前一页面 m_nextPage 该数据页的后一页面 */ pminlen = 8 m_slotCnt = 1 m_freeCnt = 5035 m_freeData = 3155 m_reservedCnt = 0 m_lsn = (39:400:68) m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0 m_tornBits = 0 DB Frag ID = 1 /* pminlen 定长数据所占的字节数为多少个字节 m_slotCnt 页面中的数据的行数 m_freeCnt 页面中剩余的空间,还剩多少字节的空间 m_freeData 页面空闲空间的起始位置,一个页面8KB约等于8192字节 页面空闲空间的位置在3155 m_reservedCnt 活动事务释放的字节数 m_lsn 日志记录号 m_xactReserved 最新加入到m_reservedCnt领域的字节数 m_xdesId 添加到m_reservedCnt的最近的事务id m_ghostRecCnt 幻影数据的行数 m_tornBits 页的校验位或者被由数据库页面保护形式决定页面保护位取代 数据库页面的 lsn SQL Server在内存中维护一个哈希表,记录下自己所有做过写入动作的页面最新的LSN(Log Sequence Number)值。 在下次读出页面的时候,会去比较这两个值是否相等。由于LSN是个自动增长的唯一值,每个发生新修改的页面, LSN的值会比原来的要大。所以如果读到的LSN与内存中存放的不一致,就说明上次的写入请求没有真正完成。 这时824错误也会被触发。 */ Allocation Status GAM (1:2) = ALLOCATED SGAM (1:3) = ALLOCATED PFS (1:1) = 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL DIFF (1:6) = CHANGED ML (1:7) = NOT MIN_LOGGED
复制代码

 

 
选项为1,输出可读形式的数据页页头数据,并且还有槽位对应记录的十六进制内容。页头数据已在上文分析,不做处理,这里截图描述槽位对应记录
复制代码
dbcc page('dbpage',1,309,1)
 
----------------------------------------------------------------------------------------------------------
页头信息省略中...
 
 
Slot 0, Offset 0x60, Length 3059, DumpStyle BYTE
/*
Slot 槽位号,一个槽位一行数据,这一行数据从 0x60 = 96开始,长度是 3059 bytes
下文文该行记录的16进制内容
*/
Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 3059                 
Memory Dump @0x000000001F978060
 
/*下文为这一行记录 3059个字节内容*/
0000000000000000:   30000800 01000000 0400a003 002b80e3 0bf38b02  0............+......
0000000000000014:   00000001 000000d5 69000070 17000033 01000001  ........i..p...3....
0000000000000028:   00000028 72707429 2c594553 204f5220 4e4f2c53  ...(rpt),YES OR NO,S
000000000000003C:   59535245 4d4f5445 4c4f4749 4e532054 59504553  YSREMOTELOGINS TYPES
中间省略...
0000000000000BCC:   7072696d 61727920 6b65792c 616e7369 5f6e756c  primary key,ansi_nul
0000000000000BE0:   6c5f6400 00d10700 00000031 01000001 000100    l_d........1.......
 
OFFSET TABLE:
 
Row - Offset                       
0 (0x0) - 96 (0x60)     
复制代码

 

选项为2, 输出整个数据页页头的十六进制数据,整一页的内容都显示,包括未使用的空间。

复制代码
dbcc page('dbpage',1,309,2)
 
----------------------------------------------------------------------------------------------------------
页头信息省略中...
 
/*下文为一整页的数据存储情况,包括行记录跟空闲空间,不区分槽位*/
DATA:
 
Memory Dump @0x0000000028178000
 
0000000028178000:   01010000 00c00001 00000000 00000800 00000000  ....................
0000000028178014:   00000100 23000000 ab13530c 35010000 01000000  ....#.....S.5.......
0000000028178028:   27000000 90010000 44000000 00000000 00000000  '.......D...........
000000002817803C:   00000000 01000000 00000000 00000000 00000000  ....................
0000000028178050:   00000000 00000000 00000000 00000000 30000800  ................0...
0000000028178064:   01000000 0400a003 002b80e3 0bf38b02 00000001  .........+..........
0000000028178078:   000000d5 69000070 17000033 01000001 00000028  ....i..p...3.......(
000000002817808C:   72707429 2c594553 204f5220 4e4f2c53 59535245  rpt),YES OR NO,SYSRE
00000000281780A0:   4d4f5445 4c4f4749 4e532054 59504553 2c535953  MOTELOGINS TYPES,SYS
省略中...
0000000028178C1C:   65726963 20726f75 6e646162 6f72742c 7072696d  eric roundabort,prim
0000000028178C30:   61727920 6b65792c 616e7369 5f6e756c 6c5f6400  ary key,ansi_null_d.
0000000028178C44:   00d10700 00000031 01000001 00010000 00212121  .......1.........!!!
0000000028178C58:   21212121 21212121 21212121 21212121 21212121  !!!!!!!!!!!!!!!!!!!!
0000000028178C6C:   21212121 21212121 21212121 21212121 21212121  !!!!!!!!!!!!!!!!!!!!
省略中...
0000000028179FE0:   21212121 21212121 21212121 21212121 21212121  !!!!!!!!!!!!!!!!!!!!
0000000028179FF4:   21212121 21212121 21216000                    !!!!!!!!!!`.
 
OFFSET TABLE:
 
Row - Offset
0 (0x0) - 96 (0x60)
复制代码

    选项为3, 输出可读形式的数据页页头数据,并且包括记录中每个字段的可读形式,行溢出数据也会显示数据内容,但是大对象则不显示内容,而是说明其存储位置!

复制代码
dbcc page('dbpage',1,309,3)
 
 
-------------------------------------------------------------------------------------------------
页头信息省略中...
 
Slot 0 Offset 0x60 Length 3059
/*
Slot 槽位号,一个槽位一行数据,这一行数据从 0x60 = 96开始,长度是 3059 bytes
*/
Record Type = PRIMARY_RECORD        Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS
Record Size = 3059
Memory Dump @0x000000002BB78060
 
/*下文为这一行记录 3059个字节内容*/
0000000000000000:   30000800 01000000 0400a003 002b80e3 0bf38b02  0............+......
0000000000000014:   00000001 000000d5 69000070 17000033 01000001  ........i..p...3....
0000000000000028:   00000028 72707429 2c594553 204f5220 4e4f2c53  ...(rpt),YES OR NO,S
000000000000003C:   59535245 4d4f5445 4c4f4749 4e532054 59504553  YSREMOTELOGINS TYPES
0000000000000050:   2c535953 52454d4f 54454c4f 47494e53 20545950  ,SYSREMOTELOGINS TYP
0000000000000064:   45532028 55504441 5445292c 41463a20 61676772  ES (UPDATE),AF: aggr
0000000000000078:   65676174 65206675 6e637469 6f6e2c41 503a2061  egate function,AP: a
中间省略...
0000000000000BB8:   2c6e756d 65726963 20726f75 6e646162 6f72742c  ,numeric roundabort,
0000000000000BCC:   7072696d 61727920 6b65792c 616e7369 5f6e756c  primary key,ansi_nul
0000000000000BE0:   6c5f6400 00d10700 00000031 01000001 000100    l_d........1.......
 
/*下文为 在槽位0 slot 0 的 这一行记录 ,详细描述每一列的存储情况*/
Slot 0 Column 1 Offset 0x4 Length 4 Length (physical) 4
/*slot 0,第一列 在本页占用4字节,列名为id,值为1*/
id = 1
 
namea = [BLOB Inline Root] Slot 0 Column 2 Offset 0x13 Length 24 Length (physical) 24
/*slot 0,第2列,本页占用24字节,列名为namea
这里可以看到是发生了行溢出情况,列中没有数据,但是存储了该列的实际位置
实际大小为6000字节,值 存储在第一个文件第307页的 slot 0 槽位上*/
 
Level = 0                           Unused = 0                          UpdateSeq = 1
TimeStamp = 1775566848              Type = 2
Link 0
 
Size = 6000                         RowId = (1:307:0)
 
Slot 0 Column 3 Offset 0x2b Length 3000 Length (physical) 3000
 
nameb = (rpt),YES OR NO,SYSREMOTELOGINS TYPES,SYSREMOTELOGINS TYPES (UPDATE),AF: aggregate function,AP: applicati
on,C : check cns,...省略中...primary key,ansi_null_d
/*slot 0,第3列 ,本页占用3000字节,列名为nameb ,值为 nameb= 的后面一大段中*/
 
descriptions = [Textpointer] Slot 0 Column 4 Offset 0xbe3 Length 16 Length (physical) 16
/*slot 0,第4列 ,该列为text数据类型,本页占用16字节,列名为descriptions,其值存储在第一个文件的第305页的 slot 1 槽位上*/
 
TextTimeStamp = 131137536           RowId = (1:305:1)
 
Slot 0 Offset 0x0 Length 0 Length (physical) 0
 
/*该表格有主键 ,该行的keyhashvalue值*/
 
KeyHashValue = (8194443284a0)
复制代码

 

    选项为3,还有一个特殊情况,就是当分析非聚集索引的时候,其返回会多一个 table 
 
复制代码
create table tbpage(id int primary key not null identity(1,1) ,cola int,colb varchar(10),colc varchar(100))
 
insert into tbpage(cola,colb,colc) select object_id,type,name from sys.objects
 
create index ix_colc on tbpage(colc)
 
select * from sys.indexes where name='ix_colc'
 
dbcc ind('dbpage','tbpage',-1)
复制代码

 

 
 
 dbcc page('dbpage',1,319,3)
返回多了一个可读性的列表,详细描述索引的列情况及键值情况,同时,原先的消息内容也还保持。

 

 

 


 
参考博文:
 
参考书籍:
《SQL SERVER 2012 实施与实战管理指南》
《SQL Server性能调优实战》
《SQL SERVER 2005 技术内幕 存储引擎》
 
如果转载,请注明博文来源: www.cnblogs.com/xinysu/ ,版权归 博客园 苏家小萝卜 所有。望各位支持!
 
 
 

4 数据行

4.1 数据行结构

    数据行在数据页面的存储结构详见下表,分为几个部分:基础信息4字节、定长列相关、变长列相关及null位图。详见下表。这部分的内容具体参考《SQL server技术内幕:存储引擎》第6章。
   参考下图,一行数据的大小是这么计算的:Row_Size=Fixed_Data_Size+Variable_Data_Size+Null_Bitmap+4 。
 
    
     各个部分其实都比较好理解,状态B位未使用,状态A位,详细描述如下。
  • 状态位A:表示行属性的位图,1字节,8bit
    • Bit 0 位,版本信息
    • Bits 1-3 位,行记录类型
      • 0,primary record,主记录
      • 1,forwarded record
      • 2,forwarding stub
      • 3,index record,索引记录
      • 4,blob或者行溢出数据
      • 5,ghost索引记录
      • 6,ghost数据记录
    • Bit 4 位,NULL位图
    • Bit 5 位,表示行中有变长列
    • Bit 6 位,保留
    • Bit 7 位,ghost record(幽灵记录)
  • 列偏移矩阵
    • 如果一个表格,没有变长列,那么这个表格则不需要列偏移矩阵
    • 一个变长列,有一个列偏移矩阵,一个列偏移矩阵2个字节,用于表示变长列中每个列的结束位置。
 

特殊情况:大对象(LOB_DATA

     text, ntext, image, nvarchar(max), varchar(max), varbinary(max), and xml这种数据列,称为大对象列, 注意,变长数据类型nvarchar,varchar,varbinary只有当存储内容大于8k才变为大对象列。
   行不能跨页,但是行的部分可以移出行所在的页,因此行实际可能非常大。页的单个行中的最大数据量和开销是 8,060 字节 (8 KB)。考虑大对象列极为占用空间,所以在一行数据的主记录中,是不存储大对象列的,仅存储 16字节 指向 大对象列实际存储到LOB data页面的位置。
    比如,一个大对象列text,text列存储5000的字符,其他列占用50个字符,如果是放在一起存储的话,10行数据就需要10个page,扫描就需要10次IO;而如果不放在一次,一个IN-ROW-DATA page就能存储这10行数据,text列单独存放在 LOB data列,那么,扫描这10行的主记录,仅需要1次IO。所以,大对象列是不跟主记录存储在一起。
 
    这样,一个8k的数据页,就能尽可能多的存储主记录,可以在查询的时候,避免 大对象列占用主记录空间,导致IO次数增增加。

特殊情况:行溢出(ROW_OVERFLOW_DATA)

    超过 8,060 字节的行大小限制可能会影响性能,因为 SQL Server 仍保持每页 8 KB 的限制。当合并 varchar、nvarchar、varbinary、sql_variant 或 CLR 用户定义类型的列超过此限制时,SQL Server 数据库引擎 将把最大宽度的记录列移动到 ROW_OVERFLOW_DATA 分配单元的另一页上,然后在主记录记录一个24字节的指针,用与描述 被移出的列 实际存储位置。比如,一行数据总大小超过8k,那么在insert的过程中,会把最大宽度的记录移动到另外的数据页面。
 
    如果更新操作使记录变长,大型记录将被动态移动到另一页。如果更新操作使记录变短,记录可能会移回 IN_ROW_DATA 分配单元中的原始页。此外,执行查询和其他选择操作(例如,对包含行溢出数据的大型记录进行排序或合并)将延长处理时间,因为这些记录将同步处理,而不是异步处理。
    一行数据(不包括大对象列)总长度超过了8k,则会把最大宽度的列内容移动到ROW_OVERFLOW_DATA页面上,主记录上留下一个24字节的指针 描述 被溢出挪走的列内容 实际存储位置,这个称为行溢出。

特殊情况:指向 forword(IN_ROW_DATA)

    在一堆表内的一个数据页面,存储了N行数据,现在,其中一行数据的某一列发生修改,导致其列的长度加大,而剩余的页面空间无法存储该列数据,那么这个时候,就会把该列数据移动到新的 IN_ROW_DATA 页面上,在主记录留下一个 9个字节的 指针,指向实际列的存储位置,这个称之为 forword。
    forward的条件是:堆表、变长列、更新操作及其数据页面剩余空间不足存储新列内容。
    为什么一定要是堆表呢?因为如果是聚集索引表格,遇到这种情况,数据页会split,把一半的内容另外存储到新的数据页,由于聚集索引上的非聚集索引键值查询根据是主键,所以split操作不会影响到非聚集索引,但是堆表的非聚集索引结构查找行是根据RID,如果也split,那么所有非聚集索引都需要修改键值RID,故在堆表上,使用了forword。
    为什么是更新操作呢?因为如果是INSERT操作,一开始就出现空间不足的情况,它老早就跑路到新的数据页上了,不会再空间不足的数据页面坐INSERT操作。
     比如,一行数据原本存储在一个数据页面中,但是update某一列,增大其存储内容,发现该数据页没有空闲的空间可以存储该列内容,该列则会forword到另外的数据页IN_ROW_DATA存储,主记录留下一个9字节的指针。

5 数据行:测试存储情况

   测试思路
  1. 先建立一个只有2列非空定长列的堆表,然后INSERT一行数据,检查page页面存储内容
  2. 添加主键,检查存储页面内容
  3. 增加一列:可空变长列
  4. 增加一列:非空变长列+默认值(分大对象和非大对象)
  5. 删除无数据的列
  6. 删除有数据的列
  7. 行溢出
  8. forword

5.1 堆表分析

 
create table tbrow(id int not null identity(1,1),name char(20) not null)
 
insert into tbrow(name) select 'xinysu';
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1) 
--根据返回结果,判断324为数据页,如果不理解,请查看本系列第一篇博文
 
dbcc page('dbpage',1,324,3)
 

    查看 `消息` 内容,可以看到 slot 0 存储的行数据大小为21字节,由于现在的 tbrow表格中,只有两列 int 跟 char ,由于都是定长列,所有变长列的存储模块均为空,但是注意一点,即使整个表格都没有允许Null的列,Null位图仍然会占用一个字节。
    所以 该行记录的长度=状态A+状态B+定长字段长度+定长字段内容+总烈属+null位图=1+1+2+(4+10)+2+1= 21 bytes。
    根据行的16进制记录:10001200 01000000 78696e79 73752020 2020020000,来详细分析这行数据的存储情况。先把这串字符按照字节数区分,其中注意部分需要反序后再转换十进制。详细分析及推导见下图。

5.2 添加主键

alter table tbrow add constraint pk_tbrow primary key(id)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
    可以看到,表格的IAM页及数据页全部都改变了,因为当一个堆表添加主键变为聚集索引表格的时候,需要重新组织数据页,按照聚集索引的键值顺序存储,所以看到,整个数据页存储情况发生了变化。如果是一个大堆表添加聚集索引,那么这是一个非常耗时及耗费IO、CPU的操作,并且会锁表直到操作结束,需谨慎操作。
    再次来分析现在的行记录。
 
dbcc page('dbpage',1,311,3)
    可以看到,数据行的内容并没有发生变化,添加主键(聚集唯一索引),会重组整个表格的存储顺序,但是不会影响到行内的数据情况。

5.3 增加一列:可空变长列

alter table tbrow add constraint pk_tbrow primary key(id)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)

    这里开始有趣了,发现,添加了一列可空可null的列后,行记录16进制并没有发生变化。对比如下。
 
/*
第一个行为堆表行记录
第二个行为添加主键后的行记录
第三个行为添加可空变长列后的行记录
 
10001200 01000000 78696e79 73752020 2020020000
10001200 01000000 78696e79 73752020 2020020000
10001200 01000000 78696e79 73752020 2020020000
*/
   
    即使表格有为null的列,有变长的列,但是,只有这些列上没有值,是不会影响这一行的数据记录的,这非常重要!因为意味着,给一个表格添加可为空的列时,存储引擎不需要去修改表格内的行记录存储情况,只需要在数据字典上添加做变动即可,这需要获取到表格的架构锁,然后执行,这个执行速度非常快。
 
    这一点的处理,跟MySQL的处理极为不一样,虽然5.6添加了OnLine DDL,避免了DDL期间对表格锁表影响,但是处理添加列的时候,涉及表结构变动,需要新建临时文件来存储frm跟ibd文件,这是一个耗费IO的处理方式,详细可查看之前博文:MySQL Online DDL的改进与应用 。

5.4 增加一列:非空变长列+默认值

5.4.1 非大对象列

alter table tbrow add task varchar(20) not null default 'all A' ;
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
 
    查看16进制的行记录:10001200 01000000 78696e79 73752020 2020020000,发现与之前的是一样的,查看表格内容,设置了NOT NULL带默认值的列后,实际上,查询出来 task列是有值存储的,存储内容为 'all A',但是查看16进制内容的时候,却发现,这个数据页内的行记录存储内容并没有发生变化。
    这是一个神奇的处理方式!为啥呢?
    仔细查看page的解析内容,发现 :Slot 0 Column 4 Offset 0x0 Length 5 Length (physical) 0 。该列数据长度为5,但是,实际存储长度为0,也就是这一列压根没有存储在数据页面中。
    个人推测:当添加了NOT NULL列+默认值(非大对象列)的情况下,不对以往数据存储记录发生修改,但是在查询的时候,会判断该列是否有存储数据,如果没有则使用默认值显示。 这样有一个非常大的好处:节约存储空间,不变更行记录,DDL期间,无需对以往记录做处理,仅需修改数据字典即可。
 
5.4.2 大对象列     
 
alter table tbrow add descriptions text not null default 'i love sql server' ;
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
    单薄的表格,一行的记录,因为添加了大对象列,来了个 LOB data的IAM页 以及 LOB data的数据页 。不过,这次仅分析主记录数据页面pageid=311。
 
--主记录数据页面pageid=311
dbcc page('dbpage',1,311,3)
 
    依旧来分析下这行存储记录,原先长度都是21,为啥添加了一个 text带默认值的列,长度就增加为50bytes呢?
 
    这里注意两个地方:原先的 task列跟 description列。task列之前是实际不存储数据内容的,但是现在存储了数据内容,description大对象列并没有存储数据在主记录中,而是存储在另外的lob data数据页中,在主记录仅存储 描述 该列具体位置内容,占16bytes。
 
    所以 该行记录的长度=状态A+状态B+定长字段长度+定长字段内容+总列数+null位图+变长列数量+列偏移矩阵+变长数据内容=1+1+2+(4+10)+2+1+2+2*3+(5+16)= 50 bytes。
 
    来看看这个16进制的字符串:30001200 01000000 78696e79 73752020 20200500 0403001d 00220032 80616c6c 20410000 d1070000 00004b01 00000100 0000,详细分析这行数据的存储情况。先把这串字符按照字节数区分,详细分析及推导见下图。

 

    由此可以得到几个推论:大对象的列NOT NULL+默认值,是在数据页上实际存储默认值的,而且会对表格中的其他原本不存储默认值的列造成影响,整个表格变成了把默认值实际存储到数据页面中去。当一个大表,需要增加一列大对象列NOT NULL+默认值时,会影响到表格里面的每一行记录,每行记录都要增加一个16字节的来描述 大对象列的存储位置,同时,原本不存储默认值的列,也会实际存储默认值到数据页面中,这是一个锁表久耗费IO的操作,对于一个大表来说。
    是不是发现自己 添加一个大对象列+默认值是一件可怕的事情?如果真有这种需求,而且还是个大表,请谨慎考虑。

5.5 删除无数据的列 

--根据之前的查询结果,skill这一列是没有存储数据的
alter table tbrow drop column skill
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
    可以发现,删除这一列,对实际数据存储并没有影响,但是该列会有一个标识值 DROPPED=[NULL]表明该列已被删除,注意,这个表示只并不是存储在每一行数据中,而是数据库存储引擎记录。
    截取数据页面里边的16进制内容:30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000,发现与删除前的是一样的,对比如下:
 
/*
 
第一个行记录为删除前
第二个行记录为删除后
 
30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000
30 00 1200 01000000 78696e79737520202020 0500 04 0300 1d0022003280 616c6c2041 0000d107000000004b01000001000000
 
*/
 
    得出结论:删除一行无数据的列时,不需要修改行内数据存储情况,仅需要修改涉及的数据字典跟删除期间持有架构锁,这是一个非常快的过程(但是如果表格一直被其他用户进行操作,那么申请架构锁也会出现等待情况)。

5.6 删除有数据的列

--根据之前的查询结果,skill这一列是没有存储数据的
alter table tbrow drop column name
 
dbcc traceon(3604)
dbcc ind('dbpage','tbrow',-1)
 
dbcc page('dbpage',1,311,3)
 
    分析到这里,可以发现,SQL SERVER在处理删除列这一块处理的非常巧妙,最大程度的减少了对表格可用性的影响,无论带不带数据,删除的时候,只处理数据字典类相关内容,标识该列已被删除,但是实际上没有去到每一个页面中去删除数据,而是把这些列占用的空间在逻辑上修改为不存在,允许以后写覆盖。
 
    作为一名小小的DBA,个人觉得在行数据的存储结构这一块,针对于增加列或者删除列的处理,SQL SERVER 设计非常巧妙及高效!相对与 MySQL改进后的Online DDL,SQL SERVER将表格的可用性大大提高以及降低对系统资源的影响。(仅讨论列的增加删除DDL这一块)

3.7 行溢出

    行溢出这块,不分析其16进制行记录,着重在 行溢出的处理方式上。
#新表格测试
create table tbflow(id int not null ,cola varchar(6000),colb varchar(6000),colc varchar(6000))
INSERT INTO tbflow SELECT 1,replicate('1',1000),replicate('1',5000),replicate('1',3000)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbflow',-1)
dbcc page('dbpage',1,334,3)
 
    cola列1000个字符,colb列5000个字符,colc列3000个字符,不算其他字节使用,光着3列长度之和就大于8k,按照行溢出的处理,可以推测出 是colb 被移动到 Row-overflow data列,所以,先分析page 334 ,看主记录的存储情况,实际情况与推测一致。

3.8 Forword

    Forword这块,不分析其16进制行记录,着重在Forword的处理方式上。
 
create table tbforword(id int not null ,cola varchar(6000),colb varchar(6000),colc varchar(6000))
insert into tbforword select 1,replicate('1',1000),replicate('1',500),replicate('1',500)
insert into tbforword select 2,replicate('1',1000),replicate('1',500),replicate('1',500)
insert into tbforword select 3,replicate('1',1000),replicate('1',500),replicate('1',500)
 
dbcc traceon(3604)
dbcc ind('dbpage','tbforword',-1) #记录 IAM是385,主记录是384页
 
update tbforword set colb=replicate('1',4500) where id=2
 
dbcc traceon(3604)
dbcc ind('dbpage','tbflow',-1)
    pageid=384数据页面中,存储3行记录大概用了6k+的空间,这时候,把id=2的colb列修改为4.5k长度,超过了一个页面8k的范围,也就意味着,这个被修改的列会被forword,根据新增的数据页386,可推测出 forword的列存储在386中。现在分析 pageid 384来验证推测。详见截图,发现与推测一致。
 
dbcc page('dbpage',1,384,3)

6 行结构与DDL

 
后半部分转自,www.coblogs.com/xinysu/
posted @ 2018-07-13 15:59  郭大侠1  阅读(497)  评论(0编辑  收藏  举报