索引修改的大致规则:
- 对表的任何修改操作(UDI),总会对表上的非聚集索引执行等价的操作。某些更新操作除外。
- 对表的任何修改操作,都会先修改堆或者聚集索引,然后再修改非聚集索引。
- 如果修改的数据行,正是过滤索引过滤掉的行(过滤索引的叶级页不包含的行),则不会对过滤索引产生任何操作。
插入数据行
对于聚集和非聚集索引的插入,新行(不管是数据行还是索引行)所包含的索引键列值就决定了它将被插入的位置。插入操作的可能来源有:
- 直接的INSERT命令
- UPDATE导致的行移动(原来的地方已经容不下被更新后的行),内部使用先DELETE,再INSERT的UPDATE策略。
- UPDATE导致的索引键列变更。索引行是有序的,行的索引键值变更会导致行在索引中的位置变更,从而需要移动到新位置。同样是先DELETE,再INSERT。
如果当前索引的叶级(叶级在聚集索引中是数据页,非聚集索引中是索引页)没有空间存放插入的新行,则索引会发生页拆分(Page Split)。行在索引中的位置是有序的,所以当新行将要被插入的某个特定页没有可用空间时,就需要分配新页给索引。会先从已经分配的区中找是未使用的页,如果没有,则会分配一个新的统一区给索引,然后再使用新区中的页。
页拆分
得到新页之后,SQL Server会尽量按照”对半分“原则,拆分原来页上的一半数据行到新页。第一次拆分是基于页上偏移阵列(Offset Array)来计算的。每次索引页拆分,还要向B+树中的父级页添加一行。有时需要多次页拆分才能将新行保存下来。页拆分发生的越多,新页也载多,需要向你级页添加的行数载多,很有可能同时导致父级页也发生页拆分。
索引树的查找方式是从根节点向叶节点进行的,所以Insert导致的页拆分也是从根节点向下发生的。这样在Insert导致的拆分未完成前,索引树需要使用闩锁(Latch)对索引进行保护,以防止索引被其它的操作修改。当从磁盘上读/写页时或者对数据页进行操作时(如页拆分),为了保护页中的数据的物理完整性,需要对页加上闩锁进行保护。当子节点的拆分完成并且不再需要对父节点进行更新时,索引树中父节点的闩锁才会被释放。
在父节点的闩锁释放前,SQL Server会检测父节点页中是否还能容纳两行新数据。如果不能,则拆分它。这种情况只会当查找索引,并且需要向索引页中添加新行时才会发生。这样做的目的是当由于子级页发生页拆分而需要向父级页插入新行时,父级页总是有空间存放这些新行。
页拆分的类型由发生拆分的页的类型决定
根页拆分
当根页发生拆分时,会分配两个新页给索引。原来根页的数据会被插入到这两个新页中。原来的根页仍然是索引的根页,它上面只有两行数据,分别指向两个新页。原来的根页被保留,可以避免修改系统目录中指向根页的指针值。根页拆分会导致索引增加新的一级索引层次(深度增加一级)。这种拆分很少发生。
中间级页拆分
中间索引页发生拆分时,会增加一个新页,然后根据索引键的中间点(Midpoint)将一半的行拆分到新页,再往父级页中插入一行指向新页。这种拆分也很少发生。
叶级页拆分
这是最常见,也是最需要关注的拆分类型。聚集索引数据页和非聚集索引叶级页的拆分机制是一样的。虽然数据页拆分只会发生在对聚集索引表执行Insert操作时,但是也可能是Update操作导致的内部Insert操作。前文提过了,当Update不是原地更新时,会执行先Delete再Insert的操作。
叶级页的拆分与中间级页的方式类似。但是需要索引管理器决定两页中的谁来接收后续的新行,还要处理两个页面谁也存不下的大型行(Large Row)。数据页拆分不会改变聚集索引键,所以相关的非聚集索引不会受到影响。
下面通过例子观察一下叶级页拆分。创建一个表,定义并插入大型行,使得一个页只能存放5行数据,然后插入第6行数据后,观察页拆分的情况。注意第6行的聚集键小于第5行。
USE test go CREATE TABLE bigrows ( a int primary key, b varchar(1600) ); GO /* Insert five rows into the table */ INSERT INTO bigrows VALUES (5, REPLICATE('a', 1600)); INSERT INTO bigrows VALUES (10, replicate('b', 1600)); INSERT INTO bigrows VALUES (15, replicate('c', 1600)); INSERT INTO bigrows VALUES (20, replicate('d', 1600)); INSERT INTO bigrows VALUES (25, replicate('e', 1600)); GO --get the data page id select allocated_page_file_id as PageFID,allocated_page_page_id as PagePID,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,168,1) go --OFFSET TABLE: --Row - Offset --4 (0x4) - 6556 (0x199c) --3 (0x3) - 4941 (0x134d) --2 (0x2) - 3326 (0xcfe) --1 (0x1) - 1711 (0x6af) --0 (0x0) - 96 (0x60) INSERT INTO bigrows VALUES (21, replicate('f', 1600)); GO select allocated_page_file_id as PageFID,allocated_page_page_id as PagePID,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,168,1) dbcc page(test,1,172,1) go --Page 168 --OFFSET TABLE: --Row - Offset --2 (0x2) - 3326 (0xcfe) --1 (0x1) - 1711 (0x6af) --0 (0x0) - 96 (0x60) --Page 172 --OFFSET TABLE: --Row - Offset --2 (0x2) - 1711 (0x6af) --1 (0x1) - 3326 (0xcfe) --0 (0x0) - 96 (0x60)