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

时间:2011-12-26 12:33:19

标签: sql sql-server performance sql-server-2008 tsql

我正在使用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倍(!)更差性能?

谢谢。

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

4 个答案:

答案 0 :(得分:121)

  

添加: SQL Server 2012在此领域显示了一些改进的性能,但似乎无法解决下面提到的具体问题。这个   应该apparently be fixed在下一个主要版本之后   SQL Server 2012!

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

我认为我会更多地研究这个,但设置一个循环(script)并尝试调整VALUES子句的数量并记录编译时间。

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

Graph

直到250个VALUES条款出现,编译时间/条款数量略有上升趋势,但没有太多戏剧性。

Graph

然后突然发生了变化。

该部分数据如下所示。

+------+----------------+-------------+---------------+---------------+
| 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个参数)的计划与非参数化计划之间的截止点。此后,似乎线性效率降低(就给定时间内处理的价值条款数量而言)。

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

当我尝试完全由重复行组成的查询时,它似乎不会影响缓存查询计划的大小,并且既不会影响常量表的输出顺序(并且当您插入到堆中时)排序的时间无论如何都是毫无意义的,即使它确实如此)。

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

Plan

我试着在调试器中查看这个,但我的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查询处理阶段的内容相关联

此阶段现在称为绑定或algebrizing,它接受前一个解析阶段的表达式解析树输出,并输出一个代数表达式树(查询处理器树)以继续优化(在这种情况下是简单的计划优化){{ 3}}。

我尝试了另外一个实验([ref]),这是为了重新运行原始测试但是看了三个不同的情况。

  1. 名字和姓氏长度为10个字符且没有重复的字符串。
  2. 名字和姓氏长度为50个字符的字符串,没有重复。
  3. 名字和姓氏长度为10个字符的字符串,包含所有重复字符。
  4. Graph

    可以清楚地看到,字符串越长越糟糕,相反,重复越多,事情就越好。如前所述,重复项不会影响缓存的计划大小,因此我假设在构造代数表达式树本身时必须存在重复标识的过程。

    修改

    利用此信息的一个地方是Script

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

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

答案 1 :(得分:22)

这并不太令人惊讶:微小插入的执行计划计算一次,然后重复使用1000次。解析和准备计划很快,因为它只有四个值。另一方面,1000行计划需要处理4000个值(如果参数化了C#测试,则需要4000个参数)。通过消除到SQL Server的999往返,这可以轻松地节省您节省的时间,特别是如果您的网络不是太慢。

答案 2 :(得分: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#开始,您可能还会考虑使用表值参数。通过用分号分隔单个批次中发出多个命令是另一种方法,也会有所帮助。

答案 3 :(得分:1)

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

由于此操作需要很长时间,因此我想将多个插入捆绑成一个(由于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

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

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

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