关于sql:多个INSERT语句与具有多个VALUES的单个INSERT

Multiple INSERT statements vs. single INSERT with multiple VALUES

我在使用1000个INSERT语句之间运行性能比较:

1
2
3
4
5
6
7
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语句:

1
2
3
4
5
6
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


Addition: SQL Server 2012 shows some improved performance in this area but doesn't seem to tackle the specific issues noted below. This
should apparently be fixed in the next major version after
SQL Server 2012!

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

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

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

Graph

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

Graph

但后来突然发生了变化。

该部分数据如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+------+----------------+-------------+---------------+---------------+
| 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结构。

典型的堆栈跟踪如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

因此,关闭堆栈跟踪中的名称似乎花费了大量时间来比较字符串。

此KB文章指出DeriveNormalizedGroupProperties与以前称为查询处理的规范化阶段的内容相关联

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

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

  • 名字和姓氏长度为10个字符的字符串,没有重复。
  • 名字和姓氏长度为50个字符的字符串,没有重复。
  • 名字和姓氏长度为10个字符的字符串,包含所有重复项。
  • Graph

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

    编辑

    @Lieven在这里展示了利用这些信息的地方

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

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


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


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

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

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


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

    由于此操作需要很长时间,因此我想将多个插入捆绑成一个(由于MSSQL限制,最多1000个)。我猜很多单个插入语句会产生类似于此处描述的开销。

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

    1
    2
    3
    4
    5
            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