SQL Server索引进阶:第十三级,插入,更新,删除

在第十级到十二级中,我们看了索引的内部结构,以及改变结构造成的影响。在本文中,继续查看Insert,update,delete和merge造成的影响。首先,我们单独看一下这四个命令。

插入INSERT

当向表中插入一行数据的时候,不管表是堆表还是聚集索引表,肯定会在表的索引中插入一个入口,过滤索引除外。这么做的时候,SQL Server使用索引键的值从根页到叶子层页,到达叶子层页之后,检查页的可用空间,如果有足够的空闲空间,新的入口就会被插入适当的位置。

最终,SQL Server可能会试图向一个已经没有空间的页插入入口信息。这时候,SQL Server就会查询位置结构,找一个有空闲空间的页。一旦找到,就会做三件事,每一件都和要插入的索引键的顺序有关:

随机序列:正常情况,SQL Server会将满页的一半的入口移动的一个空页,然后将新入口插入合适的页,这就产生了两个用了一半的页。如果你的应用继续插入数据,但是不删除数据,这两个页将会从用了一半的状态变成满页,然后再被分成两个半页,然后再次变成满页,这样周而复始,循环往复。每页的充满率大概是75%。

增序序列:SQL Server发现新的入口需要插入满页的最后面,就会新建一页,然后插入这个新入口,新页再次满了的话,就再新建一页。一旦一页满了之后,他就一直是满的,所以内部碎片很小,甚至没有。

降序序列:相反,如果SQL Server发现新的入口需要插入满页的开始,也会新建一页,插入新的入口,但是由于是降序,内部碎片接近100%。

删除DELETE

当从表中删除一行的时候,对应的索引入口会从索引中删除。对每一个索引,SQL Server为了查找入口,都要从根页导航到叶子层页。一旦SQL Server发现入口,就会做两件事:立即删除入口,或者是在行的头部设置标记,使得入口变成ghost record,在适当的时候,就会删除ghost record。

Ghost record在查询的时候,会被忽略。它们只是在物理上还存在,逻辑上已经不存在了。一个索引的ghost record的数量可以通过系统函数sys.dm_db_index_physical_stats来获取。

SQL Server没有立即删除是出于性能和并发管理的需要。不仅仅是删除本身的性能,也包括随后的事务回滚性能。上面的做法使得回滚一个删除操作是很容易的,相比较从事务日志中重新创建记录而言。

下面的因素会影响删除的处理过程:

  • 如果行被锁定,删除的索引会变成ghost record。
  • 如果执行的过程需要锁定5000行数据,行级别的锁会升级为表级别的锁。
  • 作为并发技术,行版本的使用,也会导致出现ghost record。
  • 直到事务完成,才会删除ghost record。
  • SQL Server的后台线程ghost-cleanup负责删除ghost record,但是,什么时候删除也是不可预期的。删除操作本身并不通知ghost-cleanup线程去这么做,随后的页扫描会将包含ghost record的页加入一个列表,ghost-cleanup线程会定期的处理这个列表。
  • ghost-cleanup线程大约每5秒钟唤醒一次。每次会清理10页。这些数字都是可以设置的。
  • 你可以通过sp_clean_db_free_space或者sp_clean_db_file_free_space来强制清理,将会删除整个数据库或者数据文件中的ghost record。

换句话说,当你删除数据行的时候,逻辑上讲已经删除了。如果没有被理解删除,只要SQL Server认为是安全的,他们就会被删除。

更新UPDATE

当更新表中数据行的时候,需要修改索引的入口。对于每一个索引入口,SQL Server会执行就地的更新,或者是删除再插入。只要有可能,SQL Server还是会使用就地更新。但是,也有一些情况不能就地更新,SQL Server就会执行删除紧跟着插入。下面是一些这方面的原因:

  • 更新要修改键列,导致索引的入口需要重新分配。
  • 更新要修改很多列,导致入口不合适在当前页。
  • 在表上有DML的触发器。

如果修改的列是索引键的一部分,入口的位置肯定要变化。入口会从旧的位置删除,以新键的顺序在新的位置插入入口。大部分情况,都会再删除之后执行插入。如果新的位置和旧的位置在同一页,有可能会就地更新。SQL Server会从根页到叶子层跑上两次,一次用来查找当前的入口位置,一次用来决定入口的新位置。

如果修改的列是聚集索引的一部分,所有的非聚集索引都需要更新,因为他们的标签是由聚集索引的键组成的。

如果修改的不是索引键的一部分,入口的位置不会改变。但是,入口的大小可能会改变。如果页中不够空间存放新的入口,更新就会变成删除再插入。

合并MERGER

在SQL Server 2008中引入了合并操作,很强大,很灵活,很好。合并操作会产生插入,更新,删除语句。合并和你写insert,udpate,delete产生的效果一样,所以在本系列中没有介绍。

MERGE 目标表 
 
USING 源表 
 
ON 匹配条件 
 
WHEN MATCHED THEN 
 
   语句 
 
WHEN NOT MATCHED THEN 
 
   语句; 

以上是MERGE的最最基本的语法,语句执行时根据匹配条件的结果,如果在目标表中 找到匹配记录则执行WHEN MATCHED THEN后面的语句,如果没有找到匹配记录则执行WHEN NOT MATCHED THEN后面的语句。注意源表可以是表,也可以是一个子查询语句。

格外强调一点,MERGE语句最后的分号是不能省略的!

MERGE ProductNew AS d 
USING 
    Product 
AS s 
ON s.ProductID = d.ProductId 
    WHEN NOT MATCHED THEN 
            INSERT( ProductID,ProductName,Price) 
                    VALUES(s.ProductID,s.ProductName,s.Price); 
MERGE ProductNew AS d 
USING 
    Product 
AS s 
ON s.ProductID = d.ProductId 
WHEN NOT MATCHED THEN 
    INSERT( ProductID,ProductName,Price) 
        VALUES(s.ProductID,s.ProductName,s.Price) 
WHEN MATCHED THEN 
    UPDATE SET d.ProductName = s.ProductName, d.Price = s.Price; 

一次性更新索引Index-at-a-Time Update

当执行插入,更新,删除语句操作表的一行的时候,SQL Server肯定会修改数据,然后修改索引。在执行完插入,更新,删除数据之后,SQL Server有两个选择:

  • 对每一行,执行完操作之后,都去修改索引。
  • 对每一行,执行完操作之后,对每个索引,将修改信息挂起在一个集合中。等所有的行都执行完操作之后,在执行挂起的索引修改集合。

第二种叫做“一次性更新索引”,是插入,更新,删除操作的一个选项。

SQL Server查询优化器将会决定采用哪一种来优化性能。如果修改的是表中的大部分行,很有可能会使用第二种。

为了证明,我们新建一张表,包含两个索引。

USE AdventureWorks; 
GO 
IF EXISTS (SELECT * 
       FROM sys.objects 
         WHERE name = FragTestII and type = U) 
BEGIN 
  DROP TABLE dbo.FragTestII; 
END 
GO 
CREATE TABLE dbo.FragTestII 
   ( 
    PKCol  int not null 
   , InfoCol nchar(64) not null 
   , CONSTRAINT PK_FragTestII_PKCol primary key nonclustered (PKCol) 
   ); 
GO 
CREATE INDEX IX_FragTestII_InfoCol 
     ON dbo.FragTestII (InfoCol); 
GO 

先执行一个插入一条记录的语句。

INSERT dbo.FragTestII 
VALUES (100000, XXXX); 

技术分享

上图的执行计划,只是显示了插入数据的过程,没有显示索引更新的信息。这是因为,上面的情况下,索引的更新是行更新的一部分。

当时,当我们插入大量数据的时候,执行计划就会不一样了。

我们先构造一个20000条记录的FragTest表,然后将FragTest的数据批量插入FragTestII表。

CREATE TABLE dbo.FragTest 
   ( 
    PKCol  int IDENTITY(1,1)  not null 
   , InfoCol nchar(64) not null 
   , CONSTRAINT PK_FragTest_PKCol primary key nonclustered (PKCol) 
   ); 
GO 
 
DECLARE @index INT 
SET @index=0 
 
WHILE (@index<20000) 
BEGIN 
    INSERT INTO dbo.FragTest(InfoCol)VALUES(123) 
     
    SET @index=@index +1 
     
END 
INSERT dbo.FragTestII 
  SELECT PKCol, InfoCol 
  FROM dbo.FragTest; 

技术分享

执行计划就是上面的样子,包含很多的操作。一类操作是表中插入数据。有两个排序,每个都包含一个插入索引的操作。

尽管是一个复杂的执行计划,排序和更新挂起的索引的单独执行的,但也是一个高效的执行计划。相比随即添加索引,有顺序的添加索引,产生的碎片会更少。

结论

在索引中插入入口会导致三种碎片,这依赖于插入入口的顺序。

从索引中删除入口,包括从聚集索引中删除,可能会立即删除入口。也可能会创建ghost record使得索引入口成为逻辑删除。ghost只是存在于叶子层。SQL Server在事务完成之后,才会删除ghost record。

更新索引可能会立即就地更新,也可能是删除后在插入。如果表中没有DML的触发器,如果更新没有重新分配入口,或者增加入口的大小,通常还是会就地更新的。

如果数据修改语句影响的是大量的行,SQL Server可能会选择一次性更新索引,先修改表,然后在更新每个索引。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。