【问题标题】:SQL Server index behaviour when doing bulk insert执行批量插入时的 SQL Server 索引行为
【发布时间】:2018-07-10 12:42:58
【问题描述】:

我有一个将多行一次插入 SQL Server 的应用程序。

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

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

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

有点奇怪的问题 - 这种情况是否有一个通用术语,例如“批量插入索引行为”?我试图谷歌几个关键字组合,没有找到任何东西。我问的原因是因为我有时会使用 Postgres,并且也想知道它的行为。

我多次尝试找到有关此主题的文章,但没有任何运气。

如果您能向我指出任何具有相关章节的文档、文章或书籍,那就太好了

【问题讨论】:

  • 有趣的问题,但它已经在网上technet.microsoft.com/en-us/library/…
  • 索引根据语句或 INSERT BULK 操作进行更新。具体如何取决于执行计划。
  • @PatrickHonorez 我看到了文章中的建议。但是,我一定错过了索引是如何更新的
  • 如果您对此非常感兴趣,没有什么比手动探索更好的了。读取事务日志(使用未记录但仍然常用的sys.fn_dblog 函数)将允许您准确地了解 SQL Server 对每个插入、批量或其他方式执行的操作。 For example.

标签: sql-server indexing bulkinsert sqlbulkcopy


【解决方案1】:

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

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 语句将为每个插入语句产生相同的计划。

我得到了一个类似的计划,其中包含通过行构造函数指定的大量行的单个 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);

这是 T-SQL BULK INSERT 语句的执行计划(使用虚拟空文件作为源)。通过BULK INSERT,SQL Server 添加了额外的查询计划运算符来优化索引插入。这些行在插入到表中后被假脱机,然后来自假脱机的行被排序并作为批量插入操作分别插入到每个索引中。这种方法减少了大型插入操作的开销。您可能还会看到针对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 Server 基于成本的索引维护策略。

编辑 2

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

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

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

【讨论】:

  • 丹,如果您将 Paul White 撰写的非常详细的文章 Optimizing T-SQL queries that change data 中的摘要添加到您的答案中,那么您的答案将是完美的。 Paul 的文章显示了更改索引的计划的所有可能变体,并详细解释了引擎是如何做到的。
  • @VladimirBaranov,是的,复制/粘贴错误。再次感谢。
  • @DanGuzman 我欠你一个道歉。该案例确实有一个聚集索引和其他索引。我选词不正确,抱歉。我更新了问题。它会改变你的解释吗?
  • @chester89,解释基本一样。我根据您的修订按答案编辑。
  • 显然 Microsoft.Data.SqlClient 确实包含一个 SqlBulkCopyColumnOrderHint。
【解决方案2】:

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

从低级别的角度来看,索引总是逐行更新,这是索引内部数据结构的结果。 SQL Server 索引是 B+ 树。没有算法可以一次更新 B+ 树索引中的几行,您需要一一更新,因为在更新或插入之前的行之前,您无法提前知道一行将去哪里。

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

【讨论】:

    【解决方案3】:
    My table has several indexes except clustered one
    

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

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

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

    所以堆表比聚集索引表有很多碎片。

    由于表也有several Non Clusetered index

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

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

    Bulk Insert with Clustered Index

    【讨论】:

    • 抱歉,我不太清楚 - 我应该说“我的表有多个索引以及集群索引”
    • @chester89,没问题,考虑我对堆表的解释。也阅读链接,我们在那里讨论类似的事情。
    猜你喜欢
    • 2021-05-10
    • 2010-10-03
    • 1970-01-01
    • 1970-01-01
    • 2010-09-24
    • 1970-01-01
    • 1970-01-01
    • 2011-08-21
    相关资源
    最近更新 更多