複数のINSERTステートメントと複数のVALUESを持つ単一のINSERT


119

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ミリ秒。
  • 1000の値を持つ1つのINSERTステートメント:2800ミリ秒。

テストは、測定にSQL Server Profilerを使用してMSSQL Management Studioで直接実行されます(また、SqlClientを使用してC#コードから同様の結果が得られます。これは、すべてのDALレイヤーのラウンドトリップを考慮するとさらに驚かされます)。

これは合理的であるか、または何らかの理由で説明できますか?どうして高速な方法だと、パフォーマンスが10倍(!)悪くなるのですか?

ありがとうございました。

編集:両方の実行プランを添付する: 実行計画


1
これらはクリーンなテストであり、何も並行して実行されておらず、繰り返されるデータはありません(もちろん、単純なキャッシュを回避するために、各クエリは異なるデータを使用しています)
Borka

1
関連するトリガーはありますか?
AK

2
プログラムをTVPに変換して、値の1000の制限を超え、パフォーマンスが大幅に向上しました。比較を行います。
パパラッツォ

回答:


126

追加: 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のパブリックシンボルが利用できないようです。代わりにUNION ALL、SQL Server 2005の同等の構成を確認する必要がありました。

典型的なスタックトレースは以下のとおりです

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と呼ばれていたものに関連していることを示しています

このステージは現在バインドまたは代数化と呼ばれ、前の解析ステージからの式解析ツリー出力を受け取り、代数化された式ツリー(クエリプロセッサツリー)を出力して最適化(この場合は簡単な計画最適化)に進みます[参照]

元のテストを再実行するためにもう1つの実験(スクリプト)を試しましたが、3つの異なるケースを調べました。

  1. 重複がない、長さが10文字の姓と名の文字列。
  2. 重複がない、長さが50文字の姓と名の文字列。
  3. 長さが10文字の姓と名の文字列で、すべて重複しています。

グラフ

文字列が長くなればなるほど、事態は悪化し、逆に、複製が多ければ多いほど、事態は改善することがはっきりとわかります。前述のように、重複はキャッシュされた計画サイズに影響を与えないので、代数表現ツリー自体を構築するときに重複識別のプロセスが必要であると思います。

編集する

この情報が活用される 1つの場所は、@ Lievenによってここに示されています

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

コンパイル時にName列に重複がないと判断できるため、1/ (ID - ID)実行時に2次式による順序付けをスキップし(プランのソートにはORDER BY列が1つしかない)、ゼロ除算エラーは発生しません。重複がテーブルに追加された場合、ソート演算子は列による2つの順序を示し、予期されるエラーが発生します。


6
マジックナンバーはNumberOfRows / ColumnCount = 250です。3つの列のみを使用するようにクエリを変更すると、変更は333で発生します。マジックナンバー1000は、キャッシュされたプランで使用されるパラメーターの最大数のようになります。リストを持つ<ParameterList>ものよりも計画を生成するほうが「簡単」であるように見え<ConstantScan><Values><Row>ます。
Mikael Eriksson

1
@MikaelEriksson-同意した。1000行の値を持つ250行は自動パラメーター化されますが、251行は自動パラメーター化されないため、違いがあるようです。なぜだかわかりません。おそらく、リテラル値の並べ替えに時間を費やして、重複があるかどうかを調べます。
マーティン・スミス

1
これはかなりおかしな問題で、私はただそれで悲しみました。これは素晴らしい答えのおかげで
未愛さ

1
@MikaelErikssonマジックナンバーはNumberOfRows * ColumnCount = 1000ですか?
パパラッツォ2012

1
@Blam-はい。要素の総数が1000(NumberOfRows * ColumnCount)を超える場合、クエリプランはの<ConstantScan><Values><Row>代わりに使用するように変更されました<ParameterList>
ミカエルエリクソン

23

これは驚くべきことではありません。小さな挿入の実行プランは1回計算され、その後1000回再利用されます。計画する必要があるのは4つの値だけなので、計画の解析と準備は迅速です。一方、1000行のプランでは、4000の値(またはC#テストをパラメーター化した場合は4000のパラメーター)を処理する必要があります。これは、特にネットワークの速度が過度に遅くない場合、SQL Serverへの999ラウンドトリップを排除することで得られる時間節約を簡単に使い果たしてしまう可能性があります。


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#からは、テーブル値パラメーターの使用も検討できます。複数のコマンドをセミコロンで区切って1つのバッチで発行することも、役立つ方法です。


1
Re:「単一のバッチで複数のコマンドを発行する」:少しは役立ちますが、多くは役立ちません。しかし、TRANSACTIONでラップする(TRANSは実際に機能するか、それともTRANSだけにする必要があるか)またはTVPを使用するという他の2つのオプションには間違いなく同意します。
ソロモンルツキー

1

C ++プログラム(MFC / ODBC)で数十万行のテーブルを変換しようとする同様の状況に遭遇しました。

この操作には非常に長い時間がかかったため、複数の挿入を1つにバンドルすることを考えました(MSSQLの制限により最大1000 )。単一のinsertステートメントが多数あると、ここで説明するものと同様のオーバーヘッドが発生すると思います

ただし、変換には実際にはかなり長い時間がかかることがわかります。

        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行の値のタプル(メソッド2)を含む複数行のINSERTステートメントでCDatabase :: ExecuteSqlを1回呼び出すよりも約2倍高速になります。

更新:それで、私が試した次のことは、1000個の個別のINSERTステートメントを単一の文字列にバンドルし、サーバーにそれを実行させることでした(方法3)。これは方法1よりも少し速いことがわかりました。

編集:Microsoft SQL Server Express Edition(64ビット)v10.0.2531.0を使用しています

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.