SQL 执行批量插入时的服务器索引行为

SQL Server index behaviour when doing bulk insert

我有一个应用程序可以一次将多行插入 SQL 服务器。

我使用 SqlBulkCopy class 或生成巨大 insert into table_name(...) values (...) 语句的自写代码。

我的 table 有几个索引和一个聚集索引。

问题是:这些索引是如何更新的?对于我插入的每一行?对于每笔交易?

有点奇怪的问题 - 这种情况是否有通用术语,例如 'bulk-insert indexing behaviour'?我尝试了 google 几个关键字组合,没有找到任何东西。我问的原因是因为我有时会使用 Postgres,并且也想知道它的行为。

我曾多次尝试找到有关该主题的文章,但都没有成功。

如果你能给我指点任何包含相关章节的文档、文章或书籍,那就太好了

My table has several indexes except clustered one

这意味着这个 table 只包含 non clustered index。 这也意味着这个 table 是 HEAP.

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

当数据被删除时,页面之间变得空闲但不会被回收,因为数据总是写在这一端。

因此堆 table 中的碎片比聚簇索引 table 多。

因为 table 也有 several Non Clusetered index.

提交后会自动重建索引。 由于索引是有序的,所以会有 Index page split.

因此,如果像 varchar(100),varchar(500) etc 这样的重数据类型被索引,那么索引页面拆分将非常频繁地发生。

The question is: how are those indexes updated? For each row I insert? For each transaction?

从底层的角度来看,索引总是逐行更新,这是索引内部数据结构的结果。 SQL 服务器索引是 B+ 树。没有算法可以同时更新 B+ 树索引中的几行,您需要一条一条地更新它们,因为在更新或插入前一行之前,您无法提前知道一行会去哪里。

但是从事务的角度来看,索引是一次性更新的,这是因为SQL服务器实现了事务语义。在默认隔离级别 READ COMMITTED 上,另一个事务在提交事务之前看不到您在批量插入操作中插入的行(索引或 table 行)。所以它看起来像是一次性插入了所有行。

您可以通过检查查询计划来了解索引是如何更新的。考虑这个堆 table 只有非聚集索引。

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);

执行计划仅显示 Table 插入运算符,因此新的非聚集索引行是在 table 插入操作本身期间插入的。大量单例 INSERT 语句将为每个插入语句生成相同的计划。

我得到了一个类似的计划,其中包含通过行构造函数指定的大量行的单个 INSERT 语句,唯一的区别是添加了 Constant Scan 运算符来发出行。

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);

这是 T-SQL BULK INSERT 语句的执行计划(使用虚拟空文件作为源)。使用 BULK INSERT、SQL 服务器添加了额外的查询计划运算符来优化索引插入。这些行在插入 table 后被假脱机,然后来自假脱机的行被排序并作为批量插入操作分别插入到每个索引中。此方法减少了大型插入操作的开销。您可能还会看到针对 INSERT...SELECT 查询的类似计划。

BULK INSERT dbo.BulkInsertTest
    FROM 'c:\Temp\BulkInsertTest.txt';

我通过使用扩展事件跟踪捕获实际计划,验证了 SqlBulkCopy 生成与 T-SQL BULK INSERT 相同的执行计划。下面是我使用的跟踪 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 提供 this blog article by Microsoft Data Platform MVP Paul White,其中详细介绍了 SQL 服务器基于成本的索引维护策略。

编辑 2

从你修改后的问题来看,你的实际情况是table有聚集索引而不是堆。该计划将类似于上面的堆示例,当然,除了将使用聚簇索引插入运算符而不是 Table Insert.

插入数据之外

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

不幸的是,System.Data.SqlClient.SqlBulkCopy 不支持通过 API 的 ORDER 提示。正如@benjol 在评论中提到的,较新的 Microsoft.Data.SqlClient.SqlBulkCopy 包括一个 ColumnOrderHints 属性,其中可以指定目标 table 聚簇索引列和排序顺序。