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

我正在运行使用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语句的..versus:

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个VALUES的INSERT语句: 2800毫秒。
  • 该测试直接在MSSQL Management Studio中使用SQL Server Profiler进行测量(我使用SqlClient从C#代码运行它,结果类似,考虑到所有DAL层往返都更令人吃惊)

    这是否合理或以某种方式解释? 怎么会,一个更快的方法会导致10倍(!) 更差的性能?

    谢谢。

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


    补充: 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版本的公共符号似乎不可用,所以我不得不查看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   
    

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

    此知识库文章指出, DeriveNormalizedGroupProperties与以前称为查询处理的规范化阶段的关联

    这个阶段现在被称为绑定或代数化,它从前一个分析阶段获取表达式分析树输出,并输出一个代数表达式树(查询处理器树)以进行优化(在这种情况下,平凡的计划优化)[ref]。

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

  • 名字和姓氏长度为10个字符的字符串,没有重复。
  • 名字和姓氏长度为50个字符的字符串,没有重复。
  • 名字和姓氏长度为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列)并且没有被零除错误被提出。 如果将重复项添加到表中,则排序运算符按列显示两个顺序,并引发预期的错误。


    这并不令人感到意外:微型插入的执行计划只计算一次,然后重新使用1000次。 解析和准备计划很快,因为它只有四个值可以删除。 另一方面,1000行计划需要处理4000个值(如果参数化了C#测试,则需要4000个参数)。 这可以轻松节省您通过消除999次往返SQL Server所节省的时间,尤其是在您的网络速度不够慢的情况下。


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

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

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

    链接地址: http://www.djcxy.com/p/94501.html

    上一篇: Multiple INSERT statements vs. single INSERT with multiple VALUES

    下一篇: row insert in Oracle?