なんらかの条件に基づいてINSERT、UPDATE、またはDELETEステートメントを実行する必要がある場合がよくあります。そして、私の質問は、クエリのパフォーマンスへの影響がコマンドの前にIF EXISTSを追加するかどうかです。
例
IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1
INSERTまたはDELETEについてはどうですか?
完全にはわかりませんが、この質問は実際には次のアトミック操作であるアップサートに関するものであるという印象を受けます。
UPDATE
ターゲット。INSERT
行をターゲットに入れます。DELETE
します。多くの場合、開発者からDBAへの変更は、次のように行ごとに単純に記述します。
-- For each row in source
IF EXISTS(<target_expression>)
IF @delete_flag = 1
DELETE <target_expression>
ELSE
UPDATE target
SET <target_columns> = <source_values>
WHERE <target_expression>
ELSE
INSERT target (<target_columns>)
VALUES (<source_values>)
これは、いくつかの理由で、あなたができる最悪のことです。
競合状態があります。行はIF EXISTS
およびそれに続くDELETE
またはUPDATE
。
それは無駄です。すべてのトランザクションについて、追加の操作が実行されています。多分それは取るに足らないことですが、それは完全にあなたがどれだけうまく索引を付けたかに依存します。
最悪なのは、これらの問題を単一行のレベルで考えて、反復モデルに従っていることです。これは、全体的なパフォーマンスに対するすべての影響の中で最も大きい(最悪の)影響を及ぼします。
非常にマイナーな(そしてマイナーを強調する)最適化の1つは、とにかくUPDATE
を試すことです。行が存在しない場合、@@ROWCOUNT
は0になり、「安全に」挿入できます。
-- For each row in source
BEGIN TRAN
UPDATE target
SET <target_columns> = <source_values>
WHERE <target_expression>
IF (@@ROWCOUNT = 0)
INSERT target (<target_columns>)
VALUES (<source_values>)
COMMIT
最悪の場合でも、これはすべてのトランザクションに対して2つの操作を実行しますが、少なくとも1つだけを実行するチャンスがあり、競合状態(種類)も排除されます。
しかし、本当の問題は、これがソースの各行に対してまだ行われていることです。
SQL Server 2008より前は、厄介な3ステージモデルを使用して、これを設定されたレベルで処理する必要がありました(行ごとではありません)。
BEGIN TRAN
INSERT target (<target_columns>)
SELECT <source_columns> FROM source s
WHERE s.id NOT IN (SELECT id FROM target)
UPDATE t SET <target_columns> = <source_columns>
FROM target t
INNER JOIN source s ON t.d = s.id
DELETE t
FROM target t
WHERE t.id NOT IN (SELECT id FROM source)
COMMIT
先ほど述べたように、これではパフォーマンスはかなりお粗末ですが、1行ずつのアプローチよりもはるかに優れています。しかし、SQL Server 2008では、ようやく [〜#〜] merge [〜#〜] 構文が導入されたので、次のようにするだけです。
MERGE target
USING source ON target.id = source.id
WHEN MATCHED THEN UPDATE <target_columns> = <source_columns>
WHEN NOT MATCHED THEN INSERT (<target_columns>) VALUES (<source_columns>)
WHEN NOT MATCHED BY SOURCE THEN DELETE;
それでおしまい。 1つのステートメント。 SQL Server 2008を使用していて、行がすでに存在するかどうかに応じて、INSERT
、UPDATE
、DELETE
のシーケンスを実行する必要がある場合-1行でも-noがあるため、MERGE
。
後で何が行われたかを知る必要がある場合は、OUTPUT
の影響を受ける行をMERGE
でテーブル変数に入れることもできます。シンプル、高速、リスクフリー。やれ。
これは、1回の更新/削除/挿入だけでは役に立ちません。
if条件の後に複数の演算子がある場合、パフォーマンスが向上する可能性があります。
最後のケースでは、よりよく書く
update a set .. where ..
if @@rowcount > 0
begin
..
end
どちらでもない
UPDATE … IF (@@ROWCOUNT = 0) INSERT
また
IF EXISTS(...) UPDATE ELSE INSERT
パターンは高い並行性の下で期待どおりに機能します。どちらも失敗する可能性があります。どちらも非常に頻繁に失敗する可能性があります。 MERGEは王様です-それははるかに優れています。ストレステストを行って、自分で確かめてみましょう。
以下は、使用するテーブルです。
CREATE TABLE dbo.TwoINTs
(
ID INT NOT NULL PRIMARY KEY,
i1 INT NOT NULL ,
i2 INT NOT NULL ,
version ROWVERSION
) ;
GO
INSERT INTO dbo.TwoINTs
( ID, i1, i2 )
VALUES ( 1, 0, 0 ) ;
IF EXISTS(…)THENパターンは、高い同時実行性で頻繁に失敗します。
次の単純なロジックを使用して、行をループに挿入または更新します。指定されたIDの行が存在する場合はそれを更新し、それ以外の場合は新しい行を挿入します。次のループはこのロジックを実装しています。 2つのタブに切り取って貼り付け、両方のタブでテキストモードに切り替えて、同時に実行します。
-- hit Ctrl+T to execute in text mode
SET NOCOUNT ON ;
DECLARE @ID INT ;
SET @ID = 0 ;
WHILE @ID > -100000
BEGIN ;
SET @ID = ( SELECT MIN(ID)
FROM dbo.TwoINTs
) - 1 ;
BEGIN TRY ;
BEGIN TRANSACTION ;
IF EXISTS ( SELECT *
FROM dbo.TwoINTs
WHERE ID = @ID )
BEGIN ;
UPDATE dbo.TwoINTs
SET i1 = 1
WHERE ID = @ID ;
END ;
ELSE
BEGIN ;
INSERT INTO dbo.TwoINTs
( ID, i1, i2 )
VALUES ( @ID, 0, 0 ) ;
END ;
COMMIT ;
END TRY
BEGIN CATCH ;
ROLLBACK ;
SELECT error_message() ;
END CATCH ;
END ;
このスクリプトを2つのタブで同時に実行すると、すぐに両方のタブで大量の主キー違反が発生します。これは、高い並行性で実行する場合のIF EXISTSパターンの信頼性の低さを示しています。
注:この例は、同時実行で使用する場合、次に使用可能な一意の値としてSELECT MAX(ID)+1またはSELECT MIN(ID)-1を使用することが安全でないことも示しています。
UPDATE
とDELETE
については、パフォーマンスにimpactがある場合はnot positiveであるので、これを行うべきではありません。
INSERT
の場合、INSERT
が例外(UNIQUE CONSTRAINT
違反など)。この場合、IF EXISTS
を使用して、より適切に処理します。
ほとんどの場合、これを行うべきではありません。競合状態を作成したトランザクションレベルに応じて、ここでの例ではそれほど問題になりませんが、データは最初の選択から更新に変更できます。そして、あなたがやったことはSQLにもっと仕事をさせることです
確実に知る最良の方法は、2つの違いをテストして、どれが適切なパフォーマンスを提供するかを確認することです。
IF EXISTS
は基本的にSELECTを実行します-UPDATEと同じです。
したがって、それはパフォーマンスの低下-更新するものが何もない場合、同じ量の作業を行い(UPDATEは選択した行と同じ行の欠如を照会することになります)、更新するものがあれば、ジュートします不要な選択を行いました。
IF EXISTS
ステートメントのパフォーマンス:
IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue)
クエリを満たすために存在するインデックスに依存します。
少なくとも例では、同じチェックを2回実行しているため、わずかな影響があります。
IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
照会する必要があります。存在するかどうかを確認し、trueの場合は次のようにします。
UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1
クエリを実行し、どれを確認する必要があります...同じ理由で2回同じチェックを行います。ここで、探している条件にインデックスが付けられている場合、それは高速であるはずですが、大きなテーブルの場合、selectを実行しているために遅延が発生する可能性があります。
これは、主に前の(時間までに)5(いいえ、6)(いいえ、7)の回答を繰り返しますが、次のようになります。
はい、おおむね持っているIF EXISTS構造は、データベースで行われる作業を2倍にします。 IF EXISTSは、最初に一致する行を見つけたときに「停止」します(すべてを見つける必要はありません)が、更新と削除については、余分で最終的には無意味な作業です。
したがって、どちらの方法でも、少なくとも1回はテーブル全体またはインデックス全体を読み取ることになります。しかし、そもそもなぜIF EXISTSに悩むのでしょうか。
UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1
または同様のDELETEは正常に機能します処理する行が見つかったかどうかに関係なく。行はなく、テーブルはスキャンされず、何も変更されません。 1行以上、テーブルがスキャンされ、修正されるべきすべてのものが修正されます。 「最初のクエリと2番目のクエリの間に別のユーザーがデータベースを変更したのか」について心配する必要はありません。
INSERTは便利な状況です。行を追加する前に、行が存在するかどうかを確認して、主キーまたは一意キーの違反を回避してください。もちろん、並行性について心配する必要があります-他の誰かがあなたと同時にこの行を追加しようとしている場合はどうでしょうか?これをすべて単一のINSERTにラップすると、暗黙のトランザクションですべてが処理されます(ACIDプロパティを思い出してください!):
INSERT Contacs (col1, col2, etc) values (val1, val2, etc) where not exists (select 1 from Contacs where col1 = val1)
IF @@rowcount = 0 then <didn't insert, process accordingly>
はい、これはパフォーマンスに影響します(パフォーマンスへの影響度は、いくつかの要因によって影響を受けます)。事実上、(例では)同じクエリを「2回」実行しています。クエリでこれを防御する必要があるかどうか、およびどのような状況で行がそこにないのかを自問してください。また、更新ステートメントを使用すると、影響を受ける行がおそらく更新されているかどうかを判断するためのより良い方法です。
MySQLを使用している場合は、 挿入...に複製 を使用できます。
IF EXISTS....UPDATE
しないでください。 1つではなく2つのスキャン/シークを強制します。
更新でWHERE句の一致が見つからない場合、更新ステートメントのコストはシーク/スキャンのみです。
一致が見つかると、もしあなたがそれが存在する場合にwで序文を書くならば、それは同じ一致を2回見つける必要があります。また、並行環境では、EXISTSに当てはまることがUPDATEには当てはまらない場合があります。
これが、UPDATE/DELETE/INSERTステートメントがWHERE句を許可する理由です。これを使って!