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 ...
ここで、<condition>は自然キーの評価になります。上記のアプローチはどれも並行性をうまく処理していないようです。同じ自然キーを持つ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に関連して何かをロックする場所は明確ではありません。
これはアトミックですか?これがSQLServerのドキュメントのどこに記載されているかわかりません。
EDIT:これはトランザクションで実行できることはわかっていますが、ファントム読み取りの問題を回避するには、トランザクションレベルをSERIALIZABLEに設定する必要があると思いますか?確かにそれはそのような一般的な問題にとってやり過ぎですか?
_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>
_
2番目の競合状態では、並行スレッドによってキーがとにかく削除されたと主張する可能性があるため、更新が失われたわけではありません。
最適な解決策は、通常、最も可能性の高いケースを試し、失敗した場合にエラーを処理することです(もちろん、トランザクション内で)。
正確性に加えて、このパターンは速度にも最適です。偽のロックアップを実行するよりも、例外を挿入して処理しようとする方が効率的です。ロックアップは論理ページ読み取り(物理ページ読み取りを意味する場合があります)を意味し、IO(論理的であっても)はSEHよりもコストがかかります。
更新 @Peter
単一のステートメントが「アトミック」ではないのはなぜですか?ささいなテーブルがあるとしましょう:
_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
PRIMARYKEY制約の違反 'PK__Test__24927208'。オブジェクト 'dbo.Test'に重複するキーを挿入できません。
何故ですか? SQLクエリプランが_DELETE ... FROM ... JOIN
_、WITH cte AS (SELECT...FROM ) DELETE FROM cte
、およびその他の多くの場合に「正しいこと」を実行するという点で正しいです。ただし、これらの場合には決定的な違いがあります。「サブクエリ」は、pdateまたはdelete操作のtargetを指します。このような場合、クエリプランは実際に適切なロックを使用します。実際、この動作は、キューを実装する場合など、特定の場合に重要です テーブルをキューとして使用 。
しかし、元の質問と私の例では、サブクエリはクエリオプティマイザによって、特別なロック保護を必要とする特別な「更新のスキャン」タイプのクエリとしてではなく、クエリのサブクエリとして認識されます。その結果、サブクエリルックアップの実行は、同時オブザーバーによる個別の操作として監視できるため、ステートメントの「アトミック」動作が中断されます。特別な予防措置を講じない限り、複数のスレッドが同じ値を挿入しようとする可能性があります。両方とも、チェック済みであり、値がまだ存在していないことを確信しています。 1つだけが成功でき、もう1つはPK違反になります。 QED。
行の存在をテストするときに、updlock、rowlock、holdlockのヒントを渡します。 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
[〜#〜] edit [〜#〜]:Remusは正しいです、where句付きの条件付き挿入は相関間の一貫した状態を保証しませんサブクエリとテーブル挿入。
おそらく、正しいテーブルヒントが一貫した状態を強制する可能性があります。 INSERT <table> WITH (TABLOCKX, HOLDLOCK)
は機能しているようですが、それが条件付き挿入のロックの最適なレベルであるかどうかはわかりません。
Remusが説明したような簡単なテストでは、_TABLOCKX, HOLDLOCK
_は、テーブルヒントがなく、PKエラーやコースがない場合の挿入量の約5倍を示しました。
元の回答、誤り:
これはアトミックですか?
はい、where句付きの条件付き挿入はアトミックであり、INSERT ... WHERE NOT EXISTS() ... UPDATE
フォームがUPSERTを実行する適切な方法です。
INSERTとUPDATEの間に_IF @@ROWCOUNT = 0
_を追加します。
_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 )。
アプリケーションロックを使用できます:(sp_getapplock) http://msdn.Microsoft.com/en-us/library/ms189823.aspx
私が見たトリックの1つは、INSERTを試して、失敗した場合はUPDATEを実行することです。