堆表修改内幕

      堆的修改需要使用到PFS页(PageFreeSpace)。PFS记录着数据页的空间使用情况。PFS页上使用1个字节(Byte)表示一个页的使用情况。一个PFS页可以表示8088个数据页,于是每8088个数据页就会有一个PFS页。一个数据文件的第二个页就是PFS页。PFS页上1个字节的结构:

堆表修改内幕_第1张图片

  • Bit 1:是否被分配并使用。比如,分配给对象的统一区,并不是区内所有的页都被使用。此位就用标示已分配区中页是否被真正使用。
  • Bit2:表示页是否来自混合区
  • Bit3:表示页是否是一个IAM页(Index Allocation Map)
  • Bit4:表示页中是否有幻影行(Ghost Record)。后台的幻影行清理进程就需要用到这个位了。只有删除索引中的数据时才会产生幻影行。
  • Bit5-7:表示页的空间被使用的情况。取值如上图所示。

插入数据行

      堆表插入新的数据行时,新行会被分配到任何有可用空间的地方。也就是说插入位置可能是表的任何位置。如果没有页有可用空间,则会从已经分配给表的统一分区中寻找未被使用的页来存储数据。如果所有区的所有页都没有空闲空间,则会分配新的统一分区给表来存储数据。

删除数据行

      直接删除之,删除方式跟删除索引中的非叶级页一样但跟。

   从堆表删除数据行后,被删除行所在的数据页并不会马上重组数据页以释放空间,只会标示这些空间可用。当插入新行需要连续的可用空间时,才会被回收利用。下面的示例从页中间删除一行:

CREATE TABLE smallrows

(

a int identity,

b char(10)

);

GO

INSERT INTO smallrows

VALUES ('row 1');

INSERT INTO smallrows

VALUES ('row 2');

INSERT INTO smallrows

VALUES ('row 3');

INSERT INTO smallrows

VALUES ('row 4');

INSERT INTO smallrows

VALUES ('row 5');

go

--get the data page id for dbcc page

SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc

FROM sys.dm_db_database_page_allocations

(db_id('test'), object_id('smallrows'), NULL, NULL, 'DETAILED');

go

dbcc traceon(3604)

dbcc page(test,1,146,1)

go

--delete the row a=3

DELETE FROM smallrows

WHERE a = 3;

GO

dbcc traceon(3604)

dbcc page(test,1,146,1)

go

观察两次dbcc page输出的OFFSET TABLE:

Row - Offset                        

4 (0x4) - 180 (0xb4)                

3 (0x3) - 159 (0x9f)                

2 (0x2) - 138 (0x8a)                

1 (0x1) - 117 (0x75)                

0 (0x0) - 96 (0x60) 

Row - Offset                        

4 (0x4) - 180 (0xb4)                

3 (0x3) - 159 (0x9f)                

2 (0x2) - 0 (0x0) 

1 (0x1) - 117 (0x75)                

0 (0x0) - 96 (0x60)

可以看出:

  • 删除前后其它行的偏移量没有变化,也就是说没有行被移动。
  • 删除后,被删除行(slot2)偏移量变成了0,表示该slot未被使用。

其实使用DBCC PAGE(TEST,1,146,2)仍然可以看到row3这一行。

清空堆表数据页上的数据,它也不会自动的释放这些页。可以通过sys.dm_db_partition_statst和sys.dm_db_

database_page_allocations观察到页的使用和分配信息是不会有变化的。要想清空页的数据并回收空间:

  • delete时使用表锁:数据页会被释放,但IAM页保留
  • truncate table:这个是针对清空表,会释放所有页,包括IAM页
  • 创建并删除一个聚集索引
  • 使用alter table ...rebuild

更新数据行

      SQL Server会自动选择最优的数据更新策略。基于受影响行数,访问数据的方式和是否需要修改索引键来选择最优的策略。更新实现方式包括:直接将旧值原地修改为新值和插入新值后删除旧值。

堆表中的数据行移动

      堆表中数据行的变长列的数据更新为更大尺寸的数据后,原来的数据页不能再存储它,就会发生数据行移动。数据行被移动到新页时,原来的位置上会放置一个转发指针(Forwarding Pointer)。这个指针指向行的现在的地址。这样的好处,就是当发生数据行移动时不需要移动页上所有的数据,只需要移动特定行并生成转发指针即可。

    下面的示例,创建包含变长列的表,然后更新一行的变长列,使得超出原来页的容量。然后观察页的转发情况。

if OBJECT_ID('bigrows') is  not null

  drop  TABLE bigrows

go

CREATE TABLE bigrows

( a int IDENTITY ,

b varchar(1600),

c varchar(1600));

GO

INSERT INTO bigrows

VALUES (REPLICATE('a', 1600), '');

INSERT INTO bigrows

VALUES (REPLICATE('b', 1600), '');

INSERT INTO bigrows

VALUES (REPLICATE('c', 1600), '');

INSERT INTO bigrows

VALUES (REPLICATE('d', 1600), '');

INSERT INTO bigrows

VALUES (REPLICATE('e', 1600), '');

GO

SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc

FROM sys.dm_db_database_page_allocations

(db_id('test'), object_id('bigrows'), NULL, NULL, 'DETAILED');

go

UPDATE bigrows

SET c = REPLICATE('x', 1600)

WHERE a = 3;

GO

SELECT allocated_page_file_id, allocated_page_page_id, page_type_desc

FROM sys.dm_db_database_page_allocations

(db_id('test'), object_id('bigrows'), NULL, NULL, 'DETAILED');

go

dbcc traceon(3604)

dbcc page(test,1,163,1)

go

然后观察Slot2的内容,发现记录类型为9个字节的转发存根(Forwarding Stub)。

Slot 2, Offset 0xcfe, Length 9, DumpStyle BYTE

Record Type =FORWARDING_STUB      Record Attributes =                 Record Size = 9

Memory Dump @0x000000000B4AACFE

0000000000000000:   04a50000 00010000 00

转发存根的16进制内容可心划分成4个部分,每部分代表的含义如下:

04-a5000000-0100-0000

转发存根标志字节位-转记录所在的页号-文件号-Slot编号

由于SQL Server采用的是Little-Endian字节序来组织字节存放的(也就说看到就是它在内存中存放的顺序),所以我们要看到数据本来的顺序和内容,还需要将之转换成Big-Endian的字节序组织的样子,即做一次高低位转换,然后才能转成10进制表示的内容。

高低位转换后:04-000000a5-0001-000

转10进制后:4-165-1-0

4就是表示这是一条转发存根,转发后记录的位置是:文件1中165号页内的Slot0上。也可以DBCC PAGE查看165号页Slot0长什么样。可以看到Record Type = FORWARDED_RECORD,表示这是一条转发记录。行内容是大量的c和x字符。

Slot 0, Offset 0x60, Length 3229, DumpStyle BYTE

Record Type =FORWARDED_RECORD     Record Attributes =  NULL_BITMAP VARIABLE_COLUMNS

Record Size = 3229                  

Memory Dump @0x000000000B4AA060

转发指针只存在于堆表上。一个转发指针不会指向另一个转发指针,如果转发记录再次被转发,则原来的转发指针会指向转发记录的新地址。一旦一个转发指针被生成,它会一直存在。有些情况会转发指针会被清除:

  • 转发记录尺寸缩小了,并且原来的页能够存放得下它,它就会回到原来的页,转发指针被清除
  • 收缩数据库。数据文件收缩不会产生任何新的转发指针,它会重新分配书签,并且会删除一些数据页。如果这些页包含转发记录和存根,会被重新组织到其它页,从而消除转发。
  • 使用ALTER Table Rebuild重建堆表
  • 转发行被删除
  • 建立聚集索引,变成聚集索引表

原地(In Place)更新

      原地更新是SQL Server的更新规则。原地更新只在原来的位置上修改受影响的字节内容。每更新一行就会向事务日志写一条记录。如果表有更新触发器或者行被标记为复制,则更新一行,会写两条事务日志记录。先写一条删除,再写一条插入记录。原地更新发生的两种情况:

  • 不需要用到转发指针的堆表更新
  • 不需要修改聚集键的聚集索引

聚集索引键存放是有序的,当修改聚集索引键的值,但不影响其排序位置时,也会是原地更新。比如某表的聚集索引列Name的值包括:Allen,Bill,Charlie。如果将Bill更新为Bily,是原地更新,将Bill更新为David,就是非原地升级(可能新行还会存在当前的数据页上)。

非原地更新

      非原地更新发生在更新聚集索引的索引键时。更新会变成先删除再插入两个操作。更新索引键也有可能是混合更新,即有些行是原地更新,其它行是非原地更新。更新聚集索引键时,SQL Server会生成一个包含删除和插入操作涉及到的所有行的列表。这个列表小的话就存在内存,大的话就存在tempdb。然后根据键值和操作符(删除或者插入)对列表排序。接下来分种情况:

  1. 如果索引键值非唯一,则先删除再插入。
  2. 如果索引键值唯一,则会将删除和插入这两个操作合并成一个更新操作。这样更高效。

 

总结

     1. 本文大部分理论基础和例子都参考和引用的《Microsoft SQL Server 2012 Internal》

     2. 有一段时间,经常被人问到”我要修改一下表需要你帮忙评估一下影响”,然后就做了一些基础知识的总结。

你可能感兴趣的:(堆表修改内幕)