47

SQL Server 2005 でアトミックな "UPSERT" (存在する場合は UPDATE、存在しない場合は INSERT) を実行するための正しいパターンは何ですか?

SO には、次の 2 つの部分からなるパターンを持つ多くのコードが表示されます (たとえば、「行が存在するかどうかを確認し、そうでない場合は挿入する」を参照)。

UPDATE ...
FROM ...
WHERE <condition>
-- race condition risk here
IF @@ROWCOUNT = 0
  INSERT ...

また

IF (SELECT COUNT(*) FROM ... WHERE <condition>) = 0
  -- race condition risk here
  INSERT ...
ELSE
  UPDATE ...

ここで、<条件> は自然キーの評価になります。上記のアプローチはどれも、並行性をうまく処理していないようです。同じ自然キーを持つ 2 つの行を持つことができない場合、上記のすべてが競合状態のシナリオで同じ自然キーを持つ行を挿入するリスクがあるようです。

私は次のアプローチを使用していますが、人々の回答のどこにも見られないことに驚いているので、何が問題なのか疑問に思っています:

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>

ここで言及されている競合状態は、以前のコードのものとは異なることに注意してください。以前のコードでは、問題はファントム読み取り (UPDATE/IF の間、または別のセッションによって SELECT/INSERT の間に挿入される行) でした。上記のコードでは、競合状態は DELETE に関係しています。(WHERE NOT EXISTS) が実行された後、INSERT が実行される前に、一致する行が別のセッションによって削除される可能性はありますか? WHERE NOT EXISTS が UPDATE と関連して何かをロックする場所は明確ではありません。

これはアトミックですか?これが SQL Server ドキュメントのどこに記載されているかわかりません。

編集: これはトランザクションで実行できることを認識していますが、ファントム読み取りの問題を回避するには、トランザクションレベルを SERIALIZABLE に設定する必要があると思いますか? 確かに、そのような一般的な問題にはやり過ぎですか?

4

5 に答える 5

28
INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
FROM <table>
WHERE NOT EXISTS
   -- race condition risk here?
   ( SELECT 1 FROM <table> WHERE <natural keys> )

UPDATE ...
WHERE <natural keys>
  • 最初の INSERT に競合状態があります。キーは、内部クエリの SELECT では存在しない可能性がありますが、INSERT では存在するため、キー違反が発生します。
  • INSERT と UPDATE の間に競合状態があります。キーは、INSERT の内部クエリでチェックインされたときに存在する可能性がありますが、UPDATE が実行されるまでには失われます。

2 番目の競合状態については、同時実行スレッドによってキーが削除された可能性があるため、実際には失われた更新ではないと主張することができます。

通常、最適な解決策は、最も可能性の高いケースを試し、失敗した場合はエラーを処理することです (もちろん、トランザクション内で)。

  • キーが欠落している可能性がある場合は、常に最初に挿入してください。一意の制約違反を処理し、フォールバックして更新します。
  • キーが存在する可能性が高い場合は、常に最初に更新してください。行が見つからない場合は挿入します。ユニーク制約違反の可能性を処理し、フォールバックして更新します。

正確性に加えて、このパターンは速度にも最適です。偽のロックアップを行うよりも、例外の挿入と処理を試みる方が効率的です。ロックアップは論理ページ読み取り (物理ページ読み取りを意味する場合もあります) を意味し、IO (論理であっても) は SEH よりも高価です。

@ピーターを更新

単一のステートメントが「アトミック」ではないのはなぜですか? 自明なテーブルがあるとしましょう:

create table Test (id int primary key);

この単一のステートメントを2つのスレッドからループで実行すると、あなたが言うように「アトミック」になり、競合状態は存在しません。

  insert into Test (id)
    select top (1) id
    from Numbers n
    where not exists (select id from Test where id = n.id); 

しかし、ほんの数秒で主キー違反が発生します。

メッセージ 2627、レベル 14、状態 1、行 4
PRIMARY KEY 制約 'PK__Test__24927208' の違反。オブジェクト 'dbo.Test' に重複するキーを挿入できません。

何故ですか?DELETE ... FROM ... JOINあなたは、SQL クエリ プランが、 、WITH cte AS (SELECT...FROM ) DELETE FROM cteおよび他の多くの場合に「正しいこと」を行うという点で正しいです。ただし、これらの場合には決定的な違いがあります。「サブクエリ」は、更新または削除操作のターゲットを参照します。このような場合、クエリ プランは実際に適切なロックを使用します。実際、この動作は、キューを実装する場合のように、特定のケースでは重要です

しかし、元の質問と私の例では、サブクエリは、特別なロック保護を必要とする特別な「更新のためのスキャン」タイプのクエリではなく、クエリのサブクエリとしてクエリオプティマイザーによって認識されます。その結果、サブクエリ ルックアップの実行は、同時実行オブザーバによって別個の操作として観察できるため、ステートメントの「アトミック」動作が中断されます。特別な予防措置を講じない限り、複数のスレッドが同じ値を挿入しようとする可能性があります。両方とも、チェック済みで、値がまだ存在しないと確信しています。成功できるのは 1 つだけで、もう 1 つが PK 違反になります。QED。

于 2010-03-26T17:29:25.170 に答える
7

行の存在をテストするときに、updlock、rowlock、holdlockのヒントを渡します。ホールドロックは、すべてのインサートがシリアル化されることを保証します。rowlockは、既存の行への同時更新を許可します。

内部ハッシュは64ビット値に対して縮退しているため、PKがbigintの場合でも、更新がブロックされる可能性があります。

begin tran -- default read committed isolation level is fine

if not exists (select * from <table> with (updlock, rowlock, holdlock) where <PK = ...>
    -- insert
else
    -- update

commit
于 2010-04-15T13:03:11.420 に答える
3

EDIT : Remus は正しいです。where 句を使用した条件付き挿入は、相関サブクエリとテーブル挿入の間の一貫した状態を保証しません。

おそらく、適切なテーブル ヒントにより、一貫した状態が強制される可能性があります。INSERT <table> WITH (TABLOCKX, HOLDLOCK)うまくいくようですが、それが条件付き挿入のロックの最適なレベルであるかどうかはわかりません。

Remus が説明したような簡単なテストTABLOCKX, HOLDLOCKでは、テーブル ヒントがなく、PK エラーやコースがない場合の挿入ボリュームの約 5 倍を示しました。

元の回答、不正解:

これはアトミックですか?

はい、where句を使用した条件付き挿入はアトミックであり、INSERT ... WHERE NOT EXISTS() ... UPDATEフォームはUPSERTを実行する適切な方法です。

IF @@ROWCOUNT = 0INSERT と UPDATE の間に追加します。

INSERT INTO <table>
SELECT <natural keys>, <other stuff...>
WHERE NOT EXISTS
   -- no race condition here
   ( SELECT 1 FROM <table> WHERE <natural keys> )

IF @@ROWCOUNT = 0 BEGIN
  UPDATE ...
  WHERE <natural keys>
END

単一のステートメントは常にトランザクション内で実行され、それ自体 ( autocommitおよびImplicit ) または他のステートメントと一緒に ( explicit ) 実行されます。

于 2010-03-27T01:01:09.097 に答える
2

私が見たトリックの 1 つは、INSERT を試して、失敗した場合は UPDATE を実行することです。

于 2010-03-26T11:57:58.510 に答える