web-dev-qa-db-ja.com

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

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

これは合理的ですか、何らかの形で説明できますか?どうして、おそらくより高速なメソッドが10倍(!)悪いパフォーマンスをもたらすのでしょうか?

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

編集:両方の実行計画を添付します: Exec Plans

116
Borka

追加:SQL Server 2012は、この領域でいくつかの改善されたパフォーマンスを示しますが、以下に示す特定の問題に取り組んでいないようです。これは 明らかに修正される 次のメジャーバージョンではafterSQL Server 2012!

あなたの計画では、単一の挿入がパラメータ化されたプロシージャ(おそらく自動パラメータ化された)を使用していることが示されているため、これらの解析/コンパイル時間は最小限でなければなりません。

私はこれをもう少し調べると思ったので、ループを設定し( script )、VALUES句の数を調整してコンパイル時間を記録しようとしました。

次に、コンパイル時間を行数で割って、句ごとの平均コンパイル時間を取得しました。結果は以下の通りです

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   

そのため、スタックトレースの名前を削除すると、文字列の比較に多くの時間を費やすように見えます。

このKB記事 は、DeriveNormalizedGroupPropertiesがかつて呼ばれていたものに関連付けられていることを示します 正規化 クエリ処理の段階

この段階は現在、バインディングまたは代数化と呼ばれ、前の解析段階から式解析ツリー出力を取得し、最適化に進むために代数化された式ツリー(クエリプロセッサツリー)を出力します(この場合は単純なプラン最適化) [ ref]

私はもう1つの実験( Script )を試みました。これは、元のテストを再実行することでしたが、3つの異なるケースを調べました。

  1. 長さ10文字の重複のない姓と名の文字列。
  2. 長さ50文字の重複のない姓と名の文字列。
  3. すべての重複を含む長さ10文字の名と姓の文字列。

Graph

弦が長ければ長いほど悪いものが得られ、逆に複製が多いほど良いものが得られることがはっきりとわかります。前述のように、重複はキャッシュされたプランのサイズに影響を与えないため、代数化された式ツリー自体を構築するときに重複の識別プロセスが必要になると思います。

編集

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

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

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

121
Martin Smith

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

22
dasblinkenlight

この問題は、おそらくクエリのコンパイルにかかる時間に関係しています。

挿入の速度を上げたい場合、本当に必要なのはトランザクションでそれらをラップすることです:

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#からは、テーブル値パラメーターの使用を検討することもできます。セミコロンで区切ることにより、単一のバッチで複数のコマンドを発行することも別のアプローチです。

9
RickNZ

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

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

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

        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の単一呼び出しの約2倍の速度です。

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

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

1
uceumern