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


119

我正在使用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)

..versus使用具有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次(!)一个所谓更快的方法将导致糟糕的表现?

谢谢。

编辑:附加两个的执行计划: 执行计划


1
这些是干净的测试,没有并行执行的内容,没有重复的数据(当然,每个查询都使用不同的数据,以避免简单的缓存)
Borka

1
是否有任何触发因素?
AK

2
我将程序转换为TVP,以超出值的1000个限制,并获得了很大的性能提升。我将进行比较。
狗仔队2012年

Answers:


126

另外: SQL Server 2012在此方面显示出一些改进的性能,但似乎无法解决以下特定问题。这显然应该 SQL Server 2012 之后的下一个主要版本中修复

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

我以为我会对此进行更多研究,所以设置一个循环(脚本)并尝试调整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版本的公共符号似乎不可用,因此我不得不查看UNION ALLSQL Server 2005 中的等效结构。

典型的堆栈跟踪如下

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   

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

该知识库文章表明DeriveNormalizedGroupProperties与查询处理的规范化阶段相关联

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

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

  1. 长度为10个字符的名字和姓氏字符串,无重复。
  2. 长度为50个字符的名字和姓氏字符串,无重复。
  3. 长度为10个字符的名字和姓氏字符串,所有重复项。

图形

可以清楚地看到,弦越长,效果越差;相反,重复越多,效果越好。如前所述,重复项不会影响缓存的计划大小,因此我认为构造代数表达式树本身时必须进行重复标识的过程。

编辑

@Lieven在此处显示了利用此信息的一个地方

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

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


6
您拥有的幻数是NumberOfRows / ColumnCount =250。将查询更改为仅使用三列,更改将发生在333。幻数1000可能类似于缓存计划中使用的最大参数数。似乎“容易”生成<ParameterList>带有<ConstantScan><Values><Row>列表的计划。
Mikael Eriksson

1
@MikaelEriksson-同意。250个具有1000个值的第1行会自动进行参数化,而251行没有自动进行参数化,因此这似乎是有区别的。不知道为什么。也许它花时间对字面值进行排序以查找重复项或包含重复项的内容。
马丁·史密斯

1
这是一个非常疯狂的问题,我为此感到悲伤。这是一个很好的答案,谢谢
2012年

1
@MikaelEriksson您是说魔术数字是NumberOfRows * ColumnCount = 1000吗?
狗仔队

1
@布拉姆-是的。当元素总数超过1000(NumberOfRows * ColumnCount)时,查询计划改为使用<ConstantScan><Values><Row>而不是<ParameterList>
Mikael Eriksson

23

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


9

这个问题可能与编译查询所花费的时间有关。

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

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#中,您可能还考虑使用表值参数。通过用分号将它们分开而在一个批处理中发出多个命令是另一种方法,也将有所帮助。


1
回复:“在一个批处理中发出多个命令”:这虽然有一点帮助,但没有太大帮助。但是我绝对同意另外两个选择,要么包装在TRANSACTION中(TRANS确实起作用,还是应该只是TRAN?)或使用TVP。
所罗门·鲁兹基

1

我遇到了类似的情况,试图使用C ++程序(MFC / ODBC)转换具有几100k行的表。

由于此操作花费了很长时间,因此我想到将多个插入绑定到一个中(由于MSSQL的限制,最多可以插入1000个)。我的猜测是,很多单个插入语句会产生类似于此处所述的开销。

但是,事实证明,转换实际上花费了更长的时间:

        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

因此,每个使用单个INSERT语句(方法1)对CDatabase :: ExecuteSql的1000次单次调用的速度大约是使用带有1000个值元组的多行INSERT语句(方法2)对CDatabase :: ExecuteSql的单次调用的两倍。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.