web-dev-qa-db-ja.com

最適化のためにINSERT、UPDATE、DELETEの前に存在する場合

なんらかの条件に基づいてINSERT、UPDATE、またはDELETEステートメントを実行する必要がある場合がよくあります。そして、私の質問は、クエリのパフォーマンスへの影響がコマンドの前にIF EXISTSを追加するかどうかです。

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)
    UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

INSERTまたはDELETEについてはどうですか?

36
Ed Gomoliako

完全にはわかりませんが、この質問は実際には次のアトミック操作であるアップサートに関するものであるという印象を受けます。

  • 行がソースとターゲットの両方に存在する場合、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を使用していて、行がすでに存在するかどうかに応じて、INSERTUPDATEDELETEのシーケンスを実行する必要がある場合-1行でも-noがあるため、MERGE

後で何が行われたかを知る必要がある場合は、OUTPUTの影響を受ける行をMERGEでテーブル変数に入れることもできます。シンプル、高速、リスクフリー。やれ。

72
Aaronaught

これは、1回の更新/削除/挿入だけでは役に立ちません。
if条件の後に複数の演算子がある場合、パフォーマンスが向上する可能性があります。
最後のケースでは、よりよく書く

update a set .. where ..
if @@rowcount > 0 
begin
    ..
end
8
burnall

どちらでもない

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を使用することが安全でないことも示しています。

4
A-K

UPDATEDELETEについては、パフォーマンスにimpactがある場合はnot positiveであるので、これを行うべきではありません。

INSERTの場合、INSERTが例外(UNIQUE CONSTRAINT違反など)。この場合、IF EXISTSを使用して、より適切に処理します。

4
van

ほとんどの場合、これを行うべきではありません。競合状態を作成したトランザクションレベルに応じて、ここでの例ではそれほど問題になりませんが、データは最初の選択から更新に変更できます。そして、あなたがやったことはSQLにもっと仕事をさせることです

確実に知る最良の方法は、2つの違いをテストして、どれが適切なパフォーマンスを提供するかを確認することです。

3
JoshBerke

IF EXISTSは基本的にSELECTを実行します-UPDATEと同じです。

したがって、それはパフォーマンスの低下-更新するものが何もない場合、同じ量の作業を行い(UPDATEは選択した行と同じ行の欠如を照会することになります)、更新するものがあれば、ジュートします不要な選択を行いました。

3
DVK

IF EXISTSステートメントのパフォーマンス:

IF EXISTS(SELECT 1 FROM mytable WHERE someColumn = someValue)

クエリを満たすために存在するインデックスに依存します。

2
Mitch Wheat

少なくとも例では、同じチェックを2回実行しているため、わずかな影響があります。

IF EXISTS(SELECT 1 FROM Contacs WHERE [Type] = 1)

照会する必要があります。存在するかどうかを確認し、trueの場合は次のようにします。

UPDATE Contacs SET [Deleted] = 1 WHERE [Type] = 1

クエリを実行し、どれを確認する必要があります...同じ理由で2回同じチェックを行います。ここで、探している条件にインデックスが付けられている場合、それは高速であるはずですが、大きなテーブルの場合、selectを実行しているために遅延が発生する可能性があります。

2
Nick Craver

これは、主に前の(時間までに)5(いいえ、6)(いいえ、7)の回答を繰り返しますが、次のようになります。

はい、おおむね持っているIF EXISTS構造は、データベースで行われる作業を2倍にします。 IF EXISTSは、最初に一致する行を見つけたときに「停止」します(すべてを見つける必要はありません)が、更新と削除については、余分で最終的には無意味な作業です。

  • そのような行が存在しない場合、IF EXISTSはこれを判別するためにフルスキャン(テーブルまたはインデックス)を行います。
  • そのような行が1つ以上存在する場合、IF EXISTSは最初の行を見つけるのに十分なテーブル/インデックスを読み取り、次にUPDATEまたはDELETEはそのテーブルを再度読み取り、再度見つけて処理します-そして表の「残りの部分」を調べて、処理する必要があるものがあるかどうかを確認します。 (適切に索引付けされていれば十分高速ですが、それでもまだです。)

したがって、どちらの方法でも、少なくとも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
Philip Kelley

はい、これはパフォーマンスに影響します(パフォーマンスへの影響度は、いくつかの要因によって影響を受けます)。事実上、(例では)同じクエリを「2回」実行しています。クエリでこれを防御する必要があるかどうか、およびどのような状況で行がそこにないのかを自問してください。また、更新ステートメントを使用すると、影響を受ける行がおそらく更新されているかどうかを判断するためのより良い方法です。

1
bleeeah

MySQLを使用している場合は、 挿入...に複製 を使用できます。

0
charles
IF EXISTS....UPDATE

しないでください。 1つではなく2つのスキャン/シークを強制します。

更新でWHERE句の一致が見つからない場合、更新ステートメントのコストはシーク/スキャンのみです。

一致が見つかると、もしあなたがそれが存在する場合にwで序文を書くならば、それは同じ一致を2回見つける必要があります。また、並行環境では、EXISTSに当てはまることがUPDATEには当てはまらない場合があります。

これが、UPDATE/DELETE/INSERTステートメントがWHERE句を許可する理由です。これを使って!

0
Peter Radocchia