多个INSERT语句与单个INSERT和多个VALUES

我正在运行使用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个VALUES的INSERT语句: 2800毫秒。

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

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

谢谢。

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

此外: SQL Server 2012在这方面显示出一些改进的性能,但似乎没有解决下面提到的具体问题。 这应该显然是 SQL Server 2012 之后的下一个主要版本中修复的!

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

我想我会多看看这一点,虽然这样设置一个循环( 脚本 ),并尝试调整VALUES子句的数量和logging编译时间。

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

图形

直到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 | +------+----------------+-------------+---------------+---------------+ 

已经线性增长的caching计划大小突然下降,但CompileTime增加了7倍,CompileMemory也增加了。 这是计划是一个自动参数化(有1,000个参数)和非参数化之间的中断点。 此后,它似乎线性效率较低(按照给定时间内处理的价值条款的数量)。

不知道这是为什么。 据推测,当编译特定文字值的计划时,它必须执行一些不能线性缩放的活动(如sorting)。

当我尝试一个完全由重复行组成的查询时,它似乎不会影响caching查询计划的大小,也不会影响常量表的输出顺序(并且在插入到用于sorting的堆时间无论如何,即使它没有意义)。

此外,如果将聚集索引添加到表中,则计划仍会显示一个明确的sorting步骤,因此在编译时似乎不会进行sorting以避免在运行时进行sorting。

计划

我试图在debugging器中查看这个,但是我的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 

所以关掉堆栈跟踪中的名字,似乎花了很多时间比较string。

这篇KB文章指出, DeriveNormalizedGroupProperties与以前被称为查询处理的规范化阶段

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

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

  1. 名字和姓氏长度为10个字符的string,没有重复。
  2. 名字和姓氏长度为50个字符的string,没有重复。
  3. 名字和姓氏长度的string10个字符与所有重复。

图形

可以清楚地看出,串越长,事情越糟糕,反之,越重复越好。 如前所述,重复项不会影响caching的计划大小,因此我认为在构build代数expression式树本身时必须存在重复识别的过程。

编辑

@Lieven在这里展示了这个信息被利用的一个地方

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

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

这并不奇怪:小插入的执行计划被计算一次,然后重新使用1000次。 parsing和准备计划是快速的,因为它只有四个值来删除。 另一方面,1000行计划需要处理4000个值(如果参数化了C#testing,则需要4000个参数)。 这可以很容易地节省您通过消除999往返SQL Server,特别是如果您的networking不是太慢的时间节省。

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

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

 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个)。 我猜测,大量的单一插入语句会产生类似于这里描述的开销。

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

  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语句捆绑到一个string中并让服务器执行(方法3)。 事实certificate,这比方法1更快一点。

编辑:我正在使用Microsoft SQL Server速成版(64位)v10.0.2531.0