批量插入时SQL Server索引的行为

7
我有一个应用程序,可以一次性将多行数据插入到SQL Server中。
我使用SqlBulkCopy类或自编写的代码生成一个庞大的insert into table_name(...) values (...)语句。
我的表有几个索引和一个聚集索引。
问题是:这些索引如何更新?对于每个插入的行?对于每个事务?
有点奇怪的问题——是否有一个通用术语来描述这种情况,比如“批量插入索引行为”?我尝试了几个关键词组合进行谷歌搜索,但没有找到任何东西。我问这个问题是因为我有时会与Postgres一起工作,想了解它的行为。
我一直在尝试寻找有关此主题的文章,但几次都没有找到。
如果您能指向任何文档、文章或具有相关章节的书籍,那就太好了。

有趣的问题,但它已经在网络上得到了解答。https://technet.microsoft.com/en-us/library/ms177445%28v=sql.105%29.aspx - iDevlop
我认为这个链接可以帮助你 -- https://technet.microsoft.com/zh-cn/library/ms177445(v=sql.105).aspx - Rahul Richhariya
3
索引会在每个语句或INSERT BULK操作之后进行更新,具体方式取决于执行计划。 - Dan Guzman
@PatrickHonorez 我看到了文章中的建议。但是,我可能错过了索引如何更新的部分。 - chester89
2
如果你对此非常感兴趣,最好进行一些手动探索。阅读事务日志(使用未记录但常用的sys.fn_dblog函数)将使您能够精确了解SQL Server在每个插入、批量或其他操作中正在执行的操作。例如 - Jeroen Mostert
显示剩余2条评论
3个回答

7
你可以通过查看查询计划来了解索引的更新情况。考虑这个只有非聚集索引的堆表。
CREATE TABLE dbo.BulkInsertTest(
      Column1 int NOT NULL
    , Column2 int NOT NULL
    , Column3 int NOT NULL
    , Column4 int NOT NULL
    , Column5 int NOT NULL
    );
CREATE INDEX BulkInsertTest_Column1 ON dbo.BulkInsertTest(Column1);
CREATE INDEX BulkInsertTest_Column2 ON dbo.BulkInsertTest(Column2);
CREATE INDEX BulkInsertTest_Column3 ON dbo.BulkInsertTest(Column3);
CREATE INDEX BulkInsertTest_Column4 ON dbo.BulkInsertTest(Column4);
CREATE INDEX BulkInsertTest_Column5 ON dbo.BulkInsertTest(Column5);
GO

以下是单例INSERT的执行计划。
INSERT INTO dbo.BulkInsertTest(Column1, Column2, Column3, Column4, Column5) VALUES
     (1, 2, 3, 4, 5);

INSERT execution plan

执行计划仅显示表插入运算符,因此新的非聚集索引行是在表插入操作本身中内在插入的。大量单例INSERT语句将为每个插入语句产生相同的计划。
通过使用行构造函数指定大量行的单个INSERT语句可以获得类似的计划,唯一的区别是添加了常量扫描运算符以发出行。
INSERT INTO dbo.BulkInsertTest(Column1, Column2, Column3, Column4, Column5) VALUES
     (1, 2, 3, 4, 5)
    ,(1, 2, 3, 4, 5)
    ,(1, 2, 3, 4, 5)
    ,...
    ,(1, 2, 3, 4, 5);

enter image description here

这是一个 T-SQL BULK INSERT 语句的执行计划(使用一个空文件作为源)。使用 BULK INSERT,SQL Server 添加了额外的查询计划操作符以优化索引插入。行被插入表后被暂存,然后从暂存区中的行进行排序并分别作为大规模插入操作插入到每个索引中。这种方法减少了大量插入操作的开销。您也可以看到类似的 INSERT...SELECT 查询计划。
BULK INSERT dbo.BulkInsertTest
    FROM 'c:\Temp\BulkInsertTest.txt';

BULK INSERT execution plan

我验证了SqlBulkCopy生成与T-SQL BULK INSERT相同的执行计划,通过使用Extended Event跟踪捕获实际计划。以下是我使用的跟踪DDL和PowerShell脚本。

跟踪DDL:

CREATE EVENT SESSION [SqlBulkCopyTest] ON SERVER 
ADD EVENT sqlserver.query_post_execution_showplan(
    ACTION(sqlserver.client_app_name,sqlserver.sql_text)
    WHERE ([sqlserver].[equal_i_sql_unicode_string]([sqlserver].[client_app_name],N'SqlBulkCopyTest') 
        AND [sqlserver].[like_i_sql_unicode_string]([sqlserver].[sql_text],N'insert bulk%') 
        ))
ADD TARGET package0.event_file(SET filename=N'SqlBulkCopyTest');
GO

PowerShell脚本:

$connectionString = "Data Source=.;Initial Catalog=YourUserDatabase;Integrated Security=SSPI;Application Name=SqlBulkCopyTest"

$dt = New-Object System.Data.DataTable;
$null = $dt.Columns.Add("Column1", [System.Type]::GetType("System.Int32"))
$null = $dt.Columns.Add("Column2", [System.Type]::GetType("System.Int32"))
$null = $dt.Columns.Add("Column3", [System.Type]::GetType("System.Int32"))
$null = $dt.Columns.Add("Column4", [System.Type]::GetType("System.Int32"))
$null = $dt.Columns.Add("Column5", [System.Type]::GetType("System.Int32"))

$row = $dt.NewRow()
[void]$dt.Rows.Add($row)
$row["Column1"] = 1
$row["Column2"] = 2
$row["Column3"] = 3
$row["Column4"] = 4
$row["Column5"] = 5

$bcp = New-Object System.Data.SqlClient.SqlBulkCopy($connectionString)
$bcp.DestinationTableName = "dbo.BulkInsertTest"
$bcp.WriteToServer($dt)

编辑

感谢 Vladimir Baranov 提供的Microsoft 数据平台 MVP Paul White 的博客文章,详细介绍了 SQL Server 的基于成本的索引维护策略。

编辑2

从您修改后的问题中我看到,您实际上的情况是一个有聚集索引的表而不是堆。计划将类似于上面的堆示例,除了数据将使用聚集索引插入运算符而不是表插入运算符进行插入。

在向具有聚集索引的表进行批量插入操作期间可以指定 ORDER 提示。当指定顺序与聚集索引的顺序匹配时,SQL Server 可以在聚集索引插入之前消除排序运算符,因为它假定数据已按提示排序。

很遗憾,System.Data.SqlClient.SqlBulkCopy不支持通过该API进行ORDER提示。正如@benjol在评论中提到的那样,较新的Microsoft.Data.SqlClient.SqlBulkCopy包括一个ColumnOrderHints属性,可以指定目标表聚集索引列和排序顺序。


Dan,如果你在你的回答中加入非常详细的文章Optimizing T-SQL queries that change data的总结,那么你的回答将会完美无缺。Paul的文章展示了所有可能改变索引计划的变体,并详细解释了引擎如何执行。 - Vladimir Baranov
@VladimirBaranov,是的,复制/粘贴错误。再次感谢。 - Dan Guzman
@DanGuzman,我要向你道歉。那个案例确实有一个聚集索引和其他索引。我用词不当,很抱歉。我已经更新了问题。这会改变你的解释吗? - chester89
@chester89,解释基本上是一样的。我根据你的修订编辑了我的答案。 - Dan Guzman
显然,Microsoft.Data.SqlClient包括SqlBulkCopyColumnOrderHint。 - Benjol
谢谢提醒,@Benjol。我已经在我的答案中添加了它。 - Dan Guzman

0
My table has several indexes except clustered one

这意味着此表仅包含非聚集索引。 这也意味着此表是HEAP

当插入数据(单个或批量)时,数据始终写入表的末尾或下一个可用页面。

当删除数据时,页面之间变为空闲,但不会被回收,因为数据总是写入末尾。

因此,堆表中的碎片比聚集索引表多得多。

由于表还具有多个非聚簇索引

提交后将进行自动索引重建。 由于索引是有序的,因此会出现索引页面拆分

因此,如果类似于varchar(100),varchar(500) 等重型数据类型被索引,那么索引页面拆分将经常发生。

使用聚集索引进行批量插入


抱歉,我表达不够清晰 - 我应该说“我的表有几个索引和一个聚集索引”。 - chester89
@chester89,没问题,考虑一下我对堆表的解释。同时阅读链接,我们在那里讨论了类似的事情。 - KumarHarsh

0
问题是:这些索引是如何更新的?每插入一行就更新一次吗?还是每个事务更新一次?
从低级别的角度来看,索引总是逐行更新的,这是索引内部数据结构的结果。SQL Server索引是B+树。没有算法可以一次性更新B+树索引中的多行,因为在更新或插入前,您无法预先知道一行将去哪里。
然而,从事务的角度来看,索引是一次性更新的,这是因为SQL Server实现了事务语义。在默认隔离级别READ COMMITTED下,另一个事务无法看到您在批量插入操作中插入的行(索引或表行),直到事务提交。因此,它看起来像是一次性插入了所有行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接