多个 INSERT 语句与具有多个 VALUES 的单个 INSERT

Posted

技术标签:

【中文标题】多个 INSERT 语句与具有多个 VALUES 的单个 INSERT【英文标题】:Multiple INSERT statements vs. single INSERT with multiple VALUES 【发布时间】:2012-01-27 23:31:20 【问题描述】:

我正在使用 1000 个 INSERT 语句进行性能比较:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)

..与使用具有 1000 个值的单个 INSERT 语句相比:

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)

令我大吃一惊的是,结果与我的想法相反:

1000 条 INSERT 语句:290 毫秒。 1 个具有 1000 个值的 INSERT 语句:2800 毫秒。

测试直接在 MSSQL Management Studio 中执行,使用 SQL Server Profiler 进行测量(我使用 SqlClient 从 C# 代码运行它得到了类似的结果,考虑到所有 DAL 层往返,这更加令人惊讶)

这可以合理或以某种方式解释吗?为什么,一个据说更快的方法会导致 10 倍(!)更差的性能?

谢谢。

编辑:为两者附加执行计划:

【问题讨论】:

这些是干净的测试,没有什么是并行执行的,没有重复的数据(每个查询都有不同的数据,当然是为了避免简单的缓存) 是否涉及任何触发器? 我将一个程序转换为 TVP 以超过 1000 的值限制,并获得了巨大的性能提升。我会进行比较。 相关:simple-talk.com/sql/performance/… 【参考方案1】:

这并不奇怪:小插入的执行计划只计算一次,然后重复使用 1000 次。解析和准备计划很快,因为它只有四个值需要处理。另一方面,一个 1000 行的计划需要处理 4000 个值(如果您对 C# 测试进行了参数化,则需要处理 4000 个参数)。通过消除 999 次 SQL Server 往返,这很容易消耗您节省的时间,尤其是在您的网络不是太慢的情况下。

【讨论】:

【参考方案2】:

问题可能与编译查询所需的时间有关。

如果您想加快插入速度,您真正需要做的是将它们包装在事务中:

BEGIN TRAN;
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0);
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1);
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999);
COMMIT TRAN;

在 C# 中,您还可以考虑使用表值参数。在一个批次中发出多个命令,通过用分号分隔它们,是另一种也有帮助的方法。

【讨论】:

Re:“在一个批次中发出多个命令”:这有点帮助,但不是很多。但我绝对同意其他两种选择,即包装在 TRANSACTION 中(TRANS 是否真的有效还是应该只是 TRAN?)或使用 TVP。【参考方案3】:

补充: SQL Server 2012 在这方面表现出了一些改进,但似乎没有解决下面提到的具体问题。这 apparently be fixed 应该在下一个主要版本之后 SQL Server 2012!

您的计划显示单个插入正在使用参数化过程(可能是自动参数化),因此这些的解析/编译时间应该最短。

我想我会对此进行更多研究,因此设置了一个循环 (script) 并尝试调整 VALUES 子句的数量并记录编译时间。

然后我将编译时间除以行数,得到每个子句的平均编译时间。结果如下

直到 250 个 VALUES 子句出现,编译时间/子句数量略有上升趋势,但没有太大的变化。

但是突然发生了变化。

该部分数据如下所示。

+------+----------------+-------------+---------------+---------------+
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows |
+------+----------------+-------------+---------------+---------------+
|  245 |            528 |          41 |          2400 | 0.167346939   |
|  246 |            528 |          40 |          2416 | 0.162601626   |
|  247 |            528 |          38 |          2416 | 0.153846154   |
|  248 |            528 |          39 |          2432 | 0.157258065   |
|  249 |            528 |          39 |          2432 | 0.156626506   |
|  250 |            528 |          40 |          2448 | 0.16          |
|  251 |            400 |         273 |          3488 | 1.087649402   |
|  252 |            400 |         274 |          3496 | 1.087301587   |
|  253 |            400 |         282 |          3520 | 1.114624506   |
|  254 |            408 |         279 |          3544 | 1.098425197   |
|  255 |            408 |         290 |          3552 | 1.137254902   |
+------+----------------+-------------+---------------+---------------+

一直线性增长的缓存计划大小突然下降,但 CompileTime 增加了 7 倍,并且 CompileMemory 猛增。这是计划是自动参数化计划(具有 1,000 个参数)与非参数化计划之间的分界点。此后,它似乎线性地降低了效率(就在给定时间内处理的价值子句的数量而言)。

不知道为什么会这样。据推测,当它为特定的文字值编译计划时,它必须执行一些不能线性扩展的活动(例如排序)。

当我尝试一个完全由重复行组成的查询时,它似乎不会影响缓存查询计划的大小,也不会影响常量表的输出顺序(并且当您插入到堆中时)即使这样做了,花费在排序上的时间也毫无意义)。

此外,如果将聚集索引添加到表中,该计划仍会显示显式排序步骤,因此它似乎不会在编译时进行排序以避免在运行时进行排序。

我试图在调试器中查看它,但我的 SQL Server 2008 版本的公共符号似乎不可用,因此我不得不查看 SQL Server 2005 中等效的 UNION ALL 构造。

下面是一个典型的堆栈跟踪

sqlservr.exe!FastDBCSToUnicode()  + 0xac bytes  
sqlservr.exe!nls_sqlhilo()  + 0x35 bytes    
sqlservr.exe!CXVariant::CmpCompareStr()  + 0x2b bytes   
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare()  + 0x18 bytes  
sqlservr.exe!CXVariant::CmpCompare()  + 0x11f67d bytes  
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion()  + 0xe2 bytes   
sqlservr.exe!CConstraintProp::PcnstrUnion()  + 0x35e bytes  
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive()  + 0x11a bytes    
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler()  + 0x18f bytes    
sqlservr.exe!CLogOpArg::DeriveGroupProperties()  + 0xa9 bytes   
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties()  + 0x40 bytes    
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x18a bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!CQuery::PqoBuild()  + 0x3cb bytes  
sqlservr.exe!CStmtQuery::InitQuery()  + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal()  + 0xf0 bytes   
sqlservr.exe!CStmtDML::Init()  + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep()  + 0x176 bytes   
sqlservr.exe!CSQLSource::FCompile()  + 0x741 bytes  
sqlservr.exe!CSQLSource::FCompWrapper()  + 0x922be bytes    
sqlservr.exe!CSQLSource::Transform()  + 0x120431 bytes  
sqlservr.exe!CSQLSource::Compile()  + 0x2ff bytes   

因此,在堆栈跟踪中取消名称似乎会花费大量时间来比较字符串。

This KB article 表示DeriveNormalizedGroupProperties 与过去称为normalization 的查询处理阶段相关联

此阶段现在称为绑定或代数化,它从前一个解析阶段输出表达式解析树并输出代数化表达式树(查询处理器树)以进行优化(在这种情况下是简单的计划优化)@987654325 @。

我又尝试了一项实验 (Script),即重新运行原始测试,但查看了三种不同的情况。

    名字和姓氏长度为 10 个字符的字符串,没有重复。 名字和姓氏长度为 50 个字符的字符串,没有重复。 名字和姓氏长度为 10 个字符的字符串,全部重复。

可以清楚地看出,字符串越长,情况越差,反之,重复次数越多,情况越好。如前所述,重复不会影响缓存的计划大小,所以我假设在构建代数表达式树本身时必须有一个重复识别的过程。

编辑

利用此信息的一个地方是shown by @Lieven here

SELECT * 
FROM (VALUES ('Lieven1', 1),
             ('Lieven2', 2),
             ('Lieven3', 3))Test (name, ID)
ORDER BY name, 1/ (ID - ID) 

因为在编译时它可以确定Name 列没有重复,它在运行时跳过第二个1/ (ID - ID) 表达式的排序(计划中的排序只有一个ORDER BY 列)并且没有除以引发零错误。如果将重复项添加到表中,则排序运算符显示两个按列排序,并引发预期的错误。

【讨论】:

您拥有的幻数是 NumberOfRows / ColumnCount = 250。将您的查询更改为仅使用三列,更改将发生在 333。幻数 1000 可能类似于使用的最大参数数在一个缓存的计划中。生成带有&lt;ParameterList&gt; 的计划似乎比带有&lt;ConstantScan&gt;&lt;Values&gt;&lt;Row&gt; 列表的计划“更容易”。 @MikaelEriksson - 同意。具有 1000 个值的 250 行会自动参数化,而 251 行则不会,因此这似乎确实有所不同。不知道为什么。也许它会花时间对文字值进行排序以查找重复项或其他内容。 这是一个非常疯狂的问题,我只是被它所困扰。这是一个很好的答案谢谢 @MikaelEriksson 你的意思是幻数是 NumberOfRows * ColumnCount = 1000 吗? @Blam - 是的。当元素总数超过 1000 (NumberOfRows * ColumnCount) 时,查询计划更改为使用 &lt;ConstantScan&gt;&lt;Values&gt;&lt;Row&gt; 而不是 &lt;ParameterList&gt;【参考方案4】:

我在尝试使用 C++ 程序 (MFC/ODBC) 转换具有数十万行的表时遇到了类似情况。

由于此操作花费了很长时间,我想将多个插入捆绑到一个中(由于MSSQL limitations,最多可插入 1000 个)。我猜很多单个插入语句会产生类似于here 描述的开销。

然而,事实证明,转换实际上花费了相当长的时间:

        Method 1       Method 2     Method 3 
        Single Insert  Multi Insert Joined Inserts
Rows    1000           1000         1000
Insert  390 ms         765 ms       270 ms
per Row 0.390 ms       0.765 ms     0.27 ms

因此,对 CDatabase::ExecuteSql 的 1000 次单次调用,每次调用都使用一条 INSERT 语句(方法 1),大约是使用多行 INSERT 语句对 CDatabase::ExecuteSql 进行单次调用的两倍(使用 1000 个值元组)(方法2).

更新:所以,接下来我尝试将 1000 个单独的 INSERT 语句捆绑到一个字符串中,并让服务器执行该语句(方法 3)。事实证明,这甚至比方法 1 还要快。

编辑:我使用的是 Microsoft SQL Server Express Edition(64 位)v10.0.2531.0

【讨论】:

以上是关于多个 INSERT 语句与具有多个 VALUES 的单个 INSERT的主要内容,如果未能解决你的问题,请参考以下文章

反射生成INSERT多个对象的SQL语句

具有多个嵌套 SELECT 的 MySQL INSERT

一个查询中的多个 INSERT INTO 语句

使用 Oracle Insert Into...Values 插入多个值

如何将多个 INSERT INTO 与准备好的语句结合起来?

如何最有效地获取多个 last_insert_id