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倍(!)性能下降?
谢谢。
编辑:附加两者的执行计划:
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!
您的计划显示单个插入使用参数化过程(可能是自动参数化),因此这些的解析/编译时间应该是最小的。
我想我会更多地研究这个,但是设置一个循环(脚本)并尝试调整
然后,我将编译时间除以行数,以获得每个子句的平均编译时间。结果如下
直到250
但后来突然发生了变化。
该部分数据如下所示。
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个参数)的计划与非参数化计划之间的截止点。此后,似乎线性效率降低(在给定时间内处理的值子句的数量)。
不知道为什么会这样。据推测,在为特定文字值编制计划时,它必须执行一些不能线性扩展的活动(例如排序)。
当我尝试完全由重复行组成的查询时,它似乎不会影响缓存查询计划的大小,并且既不会影响常量表的输出顺序(并且当您插入堆时排序时)即使它确实如此,也毫无意义)。
此外,如果将聚簇索引添加到表中,则计划仍显示显式排序步骤,因此它似乎在编译时不进行排序以避免在运行时进行排序。
我试图在调试器中查看这个,但我的SQL Server 2008版本的公共符号似乎不可用,所以我不得不查看SQL Server 2005中的等效
典型的堆栈跟踪如下
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文章指出
此阶段现在称为绑定或algebrizing,它从前一个解析阶段获取表达式解析树输出,并输出一个代数表达式树(查询处理器树)以继续优化(在这种情况下是简单的计划优化)[ref]。
我尝试了另外一个实验(脚本),重新运行原始测试,但看了三个不同的情况。
可以清楚地看到,字符串越长越糟糕,相反,重复越多,事情就越好。如前所述,重复项不会影响缓存的计划大小,因此我假设在构造代数表达式树本身时必须存在重复标识的过程。
编辑
@Lieven在这里展示了利用这些信息的地方
1 2 3 4 5 | SELECT * FROM (VALUES ('Lieven1', 1), ('Lieven2', 2), ('Lieven3', 3))Test (name, ID) ORDER BY name, 1/ (ID - ID) |
因为在编译时它可以确定
这并不太令人惊讶:微小插入的执行计划计算一次,然后重复使用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