堆表修改内幕

堆的修改需要使用到PFS页(PageFreeSpace)。PFS记录着数据页的空间使用情况。PFS页上使用1个字节(Byte)表示一个页的使用情况。一个PFS页可以表示8088个数据页,于是每8088个数据页就会有一个PFS页。一个数据文件的第二个页就是PFS页。PFS页上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. 有一段时间,经常被人问到”我要修改一下表需要你帮忙评估一下影响”,然后就做了一些基础知识的总结。

时间: 2024-10-29 19:12:35

堆表修改内幕的相关文章

索引深入浅出:非聚集索引的B树结构在堆表

在“索引深入浅出:非聚集索引的B树结构在聚集表”里,我们讨论了在聚集表上的非聚集索引,这篇文章我们讨论下在堆表上的非聚集索引. 非聚集索引可以在聚集表或堆表上创建.当我们在聚集表上创建非聚集索引时,聚集索引键担当为行指针.在堆表里,文件号,页号和槽号(file id , page number and slot number)的组合在非聚集索引里担当为行指针. 我们来看下手头的一个例子.我们创建salesorderdetail表的副本,并在上面的productid和salesorderid 列创

索引修改内幕

索引修改的大致规则: 对表的任何修改操作(UDI),总会对表上的非聚集索引执行等价的操作.某些更新操作除外. 对表的任何修改操作,都会先修改堆或者聚集索引,然后再修改非聚集索引. 如果修改的数据行,正是过滤索引过滤掉的行(过滤索引的叶级页不包含的行),则不会对过滤索引产生任何操作. 插入数据行 对于聚集和非聚集索引的插入,新行(不管是数据行还是索引行)所包含的索引键列值就决定了它将被插入的位置.插入操作的可能来源有: 直接的INSERT命令 UPDATE导致的行移动(原来的地方已经容不下被更新后

Oracle库Delete删除千万以上普通堆表数据的方法

需求:Oracle数据库delete删除普通堆表千万条历史记录. 直接删除的影响: 1.可能由于undo表空间不足从而导致最终删除失败的问题: 2.可能导致undo表空间过度使用,影响到其他用户正常操作. 改进方案:每删除1k行就提交一次.(这样就把一个大事物拆分成了若干个小事物) 注意:下面方法以删除2014年之前的所有记录为例,请根据你的实际情况修改,防止误操作. 方法1 declare cursor [del_cursor] is select a.*, a.rowid row_id fr

【基本优化实践】【1.2】索引优化——查看堆表、查看索引使用情况、查看索引碎片率

[1]查看堆表 --查看堆表且行大于等于10W的 select * from ( SELECT tables.NAME, (SELECT rows FROM sys.partitions WHERE object_id = tables.object_id AND index_id = 0 -- 0 is for heap -- 1 is for clustered index And rows >=100000 )AS numberofrows FROM db_tank.sys.tables

验证堆表(heap table)存储方式

验证堆表(heap table)存储方式 堆表(heap table)的存储方式: Oralce 数据库系统中最普通,最为常用的即为堆表.     堆表的数据存储方式为无序存储,也就是任意的DML操作都可能使得当前数据块存在可用的空闲空间.     处于节省空间的考虑,块上的可用空闲空间会被新插入的行填充,而不是按顺序填充到最后被使用的块上.     上述的操作方式导致了数据的无序性的产生.     当创建索引时,会根据指定的列按顺序来填充到索引块,缺省的情况下为升序.     新建或重建索引时

玩转VC++实现程序开机运行及注册表修改

 一.方案 要实现Windows程序开机运行,需要在注册表中相关位置加入键值.所谓的键可以是你程序的名称,值就是你程序的所在目录.所谓的相关位置有两处: 主键HKEY_LOCAL_MACHINE,Software\Microsoft\Windows\CurrentVersion\Run下. 主键HKEY_CURRENT_USER, Software\Microsoft\Windows\CurrentVersion\Run下. 区别就是前者针对机器上所有用户,而后者只针对当前用户. 在编程中对

Sybase数据库,普通表修改分区表步骤

本文目标:指导项目侧人员再遇到此类改动需求时可以自己参照更改.需求:Sybase数据库,普通表t_jingyu修改为按天分区的分区表. 1.sp_help查看t_jingyu的表结构,索引等信息 ? 1 2 sp_help t_jingyu go 提示:可以直接用DBArtisan工具Extract原建表语句参考 2.sp_rename重命名普通表t_jingyu及其主键pk_t_jingyu和索引idx_t_jingyu_1. ? 1 2 3 4 5 6 sp_rename t_jingyu,

SQL Server 向堆表中插入数据的过程

堆表中  IAM 记录着的数据页,表的各个数据页之间没有联系.也就是说一个页面它不会知道自己的前一页是谁,也不知道自己的后一页是谁. 插入数据时先找到IAM页,再由pfs(page free space)决定插入到哪里!

[译]SQL Passion Week 5: 堆表

SQL Passion Week 5: 堆表 今天我们介绍下所谓的堆表(Heap table), 堆表就是没有聚集索引的表. 在SQL Server中,一个表如果包含聚集索引, 我们就称为索引表, 否则就称为堆表. 在堆表中, 数据是无序的, 它们只是杂乱的放在一起, 没有结构性. 当我们select一个堆表时, 如果没有合适的非聚集索引, SQL Server会使用表扫描(Table Scan)操作来检索数据, 而不是表查找(Table Seek). 表扫描意味着将扫描整个表, 数据越多, 耗