多个 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 语句:

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倍于(!) worse性能的结果?

谢谢你。

编辑: 附上两者的执行计划: Exec Plans

122082 次浏览

这并不太令人惊讶: 微型插入的执行计划只计算一次,然后重用1000次。解析和准备计划是快速的,因为它只有四个值要处理。另一方面,一个1000行的计划需要处理4000个值(或者参数化 C # 测试的4000个参数)。这可以很容易地消除999次到 SQLServer 的往返,从而节省时间,特别是在网络速度不是太慢的情况下。

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

如果您想加快插入的速度,那么您真正需要做的是将它们包装到事务中:

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 # 中,您还可以考虑使用表值参数。通过用分号分隔多个命令,在一个批处理中发出多个命令,这是另一种也有帮助的方法。

Addition: SQL Server 2012 shows some improved performance in this area but doesn't seem to tackle the specific issues noted below. This 应 显然是被修好了在下一个主要版本 之后 SQL Server 2012!

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

I thought I'd look into this a bit more though so set up a loop (剧本) and tried adjusting the number of VALUES clauses and recording the compile time.

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

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

So going off the names in the stack trace it appears to spend a lot of time comparing strings.

这篇知识库文章 指出,DeriveNormalizedGroupProperties与过去被称为查询处理的 normalization阶段相关联

This stage is now called binding or algebrizing and it takes the expression parse tree output from the previous parse stage and outputs an algebrized expression tree (query processor tree) to go forward to optimization (trivial plan optimization in this case) [ref].

我又做了一个实验(剧本) ,重新运行最初的测试,但是观察了三个不同的案例。

  1. 名字和姓氏长度为10个字符的字符串,没有重复。
  2. First Name and Last Name Strings of length 50 characters with no duplicates.
  3. 名字和姓氏长度为10个字符的字符串,全部重复。

Graph

可以清楚地看到,字符串越长,情况就越糟糕,反之,复制的越多,情况就越好。正如前面提到的,重复不会影响缓存计划的大小,所以我假设在构造代数表达式树本身时,必须有一个重复标识的过程。

剪辑

利用此信息的一个地方是 图片由@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列) ,并且没有引发除以零的错误。如果向表中添加了重复项,那么排序操作符将按列显示两个顺序,并引发预期的错误。

我曾经遇到过类似的情况,试图用 C + + 程序(MFC/ODBC)转换一个包含几个100k 行的表。

由于这个操作花费了很长的时间,我认为应该将多个插入绑定到一个插入中(由于 MSSQL 限制的原因,最多可以达到1000个插入)。我猜想许多单独的插入语句会产生类似于所描述的 给你的开销。

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

        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

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

更新: 因此,接下来我尝试将1000个单独的 INSERT 语句绑定到一个字符串中,并让服务器执行该语句(方法3)。这比方法1还要快一点。

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