SQL ServerでのINSERT OR UPDATEのソリューション


598

のテーブル構造を想定しますMyTable(KEY, datafield1, datafield2...)

多くの場合、既存のレコードを更新するか、存在しない場合は新しいレコードを挿入します。

基本的に:

IF (key exists)
  run update command
ELSE
  run insert command

これを書くのに最も良い方法は何ですか?



27
初めてこの質問に出くわした人のために-すべての回答とコメントを必ず読んでください。年齢は時々誤解を招く情報につながる可能性があります...
アーロンバートランド

1
SQL Server 2005で導入された演算子、EXCEPT使用することを検討してください
ターザン

回答:


370

トランザクションを忘れないでください。パフォーマンスは良好ですが、単純な(IF EXISTS ..)アプローチは非常に危険です。
複数のスレッドがInsert-or-updateを実行しようとすると、主キー違反が簡単に発生します。

@Beau Crawfordと@Estebanが提供するソリューションは、一般的なアイデアを示していますが、エラーが発生しやすくなっています。

デッドロックとPK違反を回避するには、次のようなものを使用できます。

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert into table (key, ...)
   values (@key, ...)
end
commit tran

または

begin tran
   update table with (serializable) set ...
   where key = @key

   if @@rowcount = 0
   begin
      insert into table (key, ...) values (@key,..)
   end
commit tran

1
最も安全ではなく、最もパフォーマンスの高いソリューションを求める質問。トランザクションはプロセスにセキュリティを追加しますが、オーバーヘッドも追加します。
ルークベネット

31
これらの方法はどちらも失敗する可能性があります。2つの並行スレッドが同じ行で同じことを行う場合、最初のスレッドは成功しますが、2番目の挿入は主キー違反のため失敗します。トランザクションは、レコードが存在したために更新が失敗した場合でも、挿入が成功することを保証しません。任意の数の同時トランザクションが成功することを保証するには、ロックを使用する必要があります。
Jean Vincent

7
BEGIN TRANの直前の "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE"ではなく、テーブルヒント( "with(xxxx)")を使用した@akuの理由
EBarr

4
@CashCow、最後の勝ち、これはINSERTまたはUPDATEが行うことになっています。最初の1つが挿入し、2番目がレコードを更新します。ロックを追加すると、非常に短い時間枠でこれが発生し、エラーが防止されます。
Jean Vincent

1
ロックヒントを使用することは悪いことだと常に思っていました。Microsoft内部エンジンにロックを指示させる必要があります。これはルールの明らかな例外ですか?

382

非常によく似た以前の質問に対する詳細な回答を見る

@Beau CrawfordはSQL 2005以下では良い方法ですが、担当者を許可する場合は、最初の担当者に依頼してください。唯一の問題は、挿入の場合、2つのIO操作であることです。

MS Sql2008 mergeは、SQL:2003標準から導入されています。

merge tablename with(HOLDLOCK) as target
using (values ('new value', 'different value'))
    as source (field1, field2)
    on target.idfield = 7
when matched then
    update
    set field1 = source.field1,
        field2 = source.field2,
        ...
when not matched then
    insert ( idfield, field1, field2, ... )
    values ( 7,  source.field1, source.field2, ... )

これは実際には1つのIO操作にすぎませんが、ひどいコード:-(


10
@Ian Boyd-そうです、それはSQL:2003標準の構文でありupsert、他のすべてのDBプロバイダーが代わりにサポートすることを決定したものではありません。upsertそれはT-SQLで唯一の非標準のキーワードのようにそうではありません-構文はとても少なくともMSがあまりにもそれをサポートしている必要があり、これを行うにはこれまでよりよい方法である
キース・

1
他の回答のロックヒントに関するコメントはありますか?(すぐにわかりますが、それが推奨される方法であれば、答えに追加することをお勧めします)
eglasius

25
構文を使用している場合でも発生する可能性のあるエラーが競合状態によって引き起こされるのを防ぐ方法については、こちらのweblogs.sqlteam.com/dang/archive/2009/01/31/…を参照してくださいMERGE
2012年

5
@Sephそれは本当に驚きです-Microsoftによる多少の失敗:-SI推測ではHOLDLOCK、同時実行性の高い状況ではマージ操作が必要です。
キース

11
この回答は、HOLDLOCKなしではスレッドセーフではないというSephのコメントを反映するように更新する必要があります。リンクされた投稿によると、MERGEは暗黙的に更新ロックを解除しますが、行を挿入する前にロックを解放するため、挿入時に競合状態と主キー違反が発生する可能性があります。HOLDLOCKを使用すると、挿入が発生するまでロックが保持されます。
Triynko 2013年

169

UPSERTを実行します。

UPDATE MyTable SET FieldA = @ FieldA WHERE Key = @ Key

IF @@ ROWCOUNT = 0
   INSERT INTO MyTable(FieldA)VALUES(@FieldA)

http://en.wikipedia.org/wiki/Upsert


7
適切な一意インデックス制約が適用されている場合、主キー違反は発生しません。制約の要点は、重複する行が発生するのを防ぐことです。挿入しようとしているスレッドの数は関係ありません。データベースは必要に応じてシリアル化して制約を適用します。そうでない場合、エンジンは無意味です。もちろん、これをシリアライズされたトランザクションでラップすることで、これがより正確になり、デッドロックや失敗した挿入の影響を受けにくくなります。
Triynko

19
@ Triynko、@ Sam Saffronは、2つ以上のスレッドが正しい順序でインターリーブした場合、SQLサーバーがプライマリキー違反発生したことを示すエラーをスローすることを意味したと思います。シリアライズ可能なトランザクションでそれをラップすることは、上記のステートメントセットのエラーを防ぐ正しい方法です。
EBarr

1
自動インクリメントである主キーがある場合でも、テーブルにある可能性のある一意の制約が問題になります。
2012年

1
データベースは、主要な問題を処理する必要があります。あなたが言っているのは、更新が失敗し、別のプロセスが挿入で最初にそこに到達した場合、挿入は失敗するということです。その場合、とにかく競合状態になります。ロックしても、事後条件は、書き込みを試行するプロセスの1つが値を取得するということになるという事実は変わりません。
CashCow

93

多くの人がを使用することを提案しますMERGEが、私はそれを警告します。デフォルトでは、複数のステートメントよりも同時実行性や競合状態からユーザーを保護することはなく、他の危険も伴います。

http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/

この「より単純な」構文が利用できる場合でも、私はこのアプローチを優先しています(簡潔にするためにエラー処理は省略しています)。

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
UPDATE dbo.table SET ... WHERE PK = @PK;
IF @@ROWCOUNT = 0
BEGIN
  INSERT dbo.table(PK, ...) SELECT @PK, ...;
END
COMMIT TRANSACTION;

多くの人がこのように提案します:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
IF EXISTS (SELECT 1 FROM dbo.table WHERE PK = @PK)
BEGIN
  UPDATE ...
END
ELSE
  INSERT ...
END
COMMIT TRANSACTION;

ただし、これにより、更新する行を見つけるためにテーブルを2回読み取る必要がある場合があります。最初のサンプルでは、​​行を見つける必要があるのは一度だけです。(どちらの場合も、最初の読み取りから行が見つからない場合、挿入が発生します。)

他の人はこのように提案します:

BEGIN TRY
  INSERT ...
END TRY
BEGIN CATCH
  IF ERROR_NUMBER() = 2627
    UPDATE ...
END CATCH

ただし、ほとんどすべての挿入が失敗するというまれなシナリオを除いて、SQL Serverに例外をキャッチさせる以外の理由で最初から防ぐことができれば、はるかにコストがかかる場合、これは問題になります。私はここで多くを証明します:


3
多くのレコードを挿入/更新するTEMテーブルからの挿入/更新についてはどうですか?
user960567

@ user960567ええと、UPDATE target SET col = tmp.col FROM target INNER JOIN #tmp ON <key clause>; INSERT target(...) SELECT ... FROM #tmp AS t WHERE NOT EXISTS (SELECT 1 FROM target WHERE key = t.key);
アーロンバートランド

4
niceは2年以上後に返信しました:)
user960567

12
@ user960567申し訳ありませんが、コメントの通知を常にリアルタイムで受信できるとは限りません。
Aaron Bertrand

60
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)

編集:

悲しいかな、私自身の不利益にも関わらず、selectなしでこれを行うソリューションは、1つの少ないステップでタスクを完了するため、より良いように思えます。


6
私はまだこれが好きです。アップサートは副作用によるプログラミングに似ており、実際のデータベースでパフォーマンスの問題を引き起こすその最初の選択の非常に小さなクラスター化インデックスシークを見たことはありません
エリックZビアード

38

一度に複数のレコードをUPSERTする場合は、ANSI SQL:2003 DMLステートメントMERGEを使用できます。

MERGE INTO table_name WITH (HOLDLOCK) USING table_name ON (condition)
WHEN MATCHED THEN UPDATE SET column1 = value1 [, column2 = value2 ...]
WHEN NOT MATCHED THEN INSERT (column1 [, column2 ...]) VALUES (value1 [, value2 ...])

SQL Server 2005のMERGEステートメントの模倣を確認してください。


1
Oracleでは、MERGEステートメントを発行するテーブルがロックされると思います。SQL * Serverでも同じですか?
マイクマカリスター

13
MERGEは、サーティアンロックを保持しない限り、競合状態(weblogs.sqlteam.com/dang/archive/2009/01/31/…を参照)の影響を受けやすくなります。また、SQLプロファイラーでのMERGEのパフォーマンスも確認してください。通常、他のソリューションよりも遅く、生成される読み取りが多いことがわかります。
EBarr

@EBarr-ロックのリンクをありがとう。回答を更新して、ロックのヒントの提案を含めました。
Eric Weilnau、2010


10

これについてコメントするのはかなり遅いですが、MERGEを使用してより完全な例を追加したいと思います。

このようなInsert + Updateステートメントは通常「Upsert」ステートメントと呼ばれ、SQL ServerのMERGEを使用して実装できます。

非常に良い例をここに示します:http : //weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx

上記では、ロックと同時実行のシナリオについても説明しています。

私は参考のために同じことを引用します:

ALTER PROCEDURE dbo.Merge_Foo2
      @ID int
AS

SET NOCOUNT, XACT_ABORT ON;

MERGE dbo.Foo2 WITH (HOLDLOCK) AS f
USING (SELECT @ID AS ID) AS new_foo
      ON f.ID = new_foo.ID
WHEN MATCHED THEN
    UPDATE
            SET f.UpdateSpid = @@SPID,
            UpdateTime = SYSDATETIME()
WHEN NOT MATCHED THEN
    INSERT
      (
            ID,
            InsertSpid,
            InsertTime
      )
    VALUES
      (
            new_foo.ID,
            @@SPID,
            SYSDATETIME()
      );

RETURN @@ERROR;

1
MERGEについては、他にも注意す
アーロンベルトラン

8
/*
CREATE TABLE ApplicationsDesSocietes (
   id                   INT IDENTITY(0,1)    NOT NULL,
   applicationId        INT                  NOT NULL,
   societeId            INT                  NOT NULL,
   suppression          BIT                  NULL,
   CONSTRAINT PK_APPLICATIONSDESSOCIETES PRIMARY KEY (id)
)
GO
--*/

DECLARE @applicationId INT = 81, @societeId INT = 43, @suppression BIT = 0

MERGE dbo.ApplicationsDesSocietes WITH (HOLDLOCK) AS target
--set the SOURCE table one row
USING (VALUES (@applicationId, @societeId, @suppression))
    AS source (applicationId, societeId, suppression)
    --here goes the ON join condition
    ON target.applicationId = source.applicationId and target.societeId = source.societeId
WHEN MATCHED THEN
    UPDATE
    --place your list of SET here
    SET target.suppression = source.suppression
WHEN NOT MATCHED THEN
    --insert a new line with the SOURCE table one row
    INSERT (applicationId, societeId, suppression)
    VALUES (source.applicationId, source.societeId, source.suppression);
GO

テーブルとフィールドの名前を必要なものに置き換えます。使用ON条件に注意してください。次に、DECLARE行の変数に適切な値(およびタイプ)を設定します。

乾杯。


7

MERGEステートメントを使用できます。このステートメントは、存在しない場合はデータを挿入し、存在する場合は更新するために使用されます。

MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`

@RamenChefわかりません。WHEN MATCHED句はどこにありますか?
likejudo

@likejudo私はこれを書きませんでした。改訂しただけです。投稿を書いたユーザーに尋ねます。
RamenChef

5

UPDATE if-no-rows-updated then INSERTルートに進む場合は、最初にINSERTを実行して競合状態を回避することを検討してください(DELETEが介在しないと仮定)

INSERT INTO MyTable (Key, FieldA)
   SELECT @Key, @FieldA
   WHERE NOT EXISTS
   (
       SELECT *
       FROM  MyTable
       WHERE Key = @Key
   )
IF @@ROWCOUNT = 0
BEGIN
   UPDATE MyTable
   SET FieldA=@FieldA
   WHERE Key=@Key
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

競合状態の回避とは別に、ほとんどの場合、レコードがすでに存在していると、INSERTが失敗し、CPUが浪費されます。

SQL2008以降では、おそらくMERGEを使用することをお勧めします。


興味深いアイデアですが、構文が正しくありません。SELECTにはFROM <table_source>とTOP 1が必要です(選択したtable_sourceに1行しかない場合を除く)。
jk7

ありがとう。NOT EXISTSに変更しました。O / Pによる「キー」のテストのため、一致する行は1つしかありません(ただし、マルチパートキーである必要があるかもしれませんが:))
Kristen

4

それは使用パターンによって異なります。詳細に迷うことなく、使用状況の全体像を確認する必要があります。たとえば、レコードの作成後に使用パターンが99%更新されている場合、「UPSERT」が最適なソリューションです。

最初の挿入(ヒット)の後は、ifsやbutsではなく、すべて単一ステートメントの更新になります。挿入の 'where'条件が必要です。それ以外の場合は重複が挿入され、ロックを処理する必要はありません。

UPDATE <tableName> SET <field>=@field WHERE key=@key;

IF @@ROWCOUNT = 0
BEGIN
   INSERT INTO <tableName> (field)
   SELECT @field
   WHERE NOT EXISTS (select * from tableName where key = @key);
END

2

MS SQL Server 2008はMERGEステートメントを導入しています。これは、SQL:2003標準の一部だと思います。多くの人が1行のケースを処理することは大きな問題ではないことを示していますが、大きなデータセットを処理する場合は、カーソルが必要であり、それに伴うすべてのパフォーマンスの問題があります。MERGEステートメントは、大規模なデータセットを処理するときに非常に歓迎される追加です。


1
大規模なデータセットでこれを行うためにカーソルを使用する必要がありませんでした。一致するレコードを更新する更新と、テーブルへの左結合である値句の代わりに選択を使用した挿入が必要です。
HLGEM 2009

1

誰もがsprocを直接実行しているこれらの悪意のあるユーザーからの恐怖からHOLDLOCK-sにジャンプする前に、設計によって新しいPK-sの一意性(IDキー、Oracleのシーケンスジェネレーター、一意のインデックス)を保証する必要があることを指摘しておきます。外部ID、インデックスでカバーされるクエリ)。それが問題のアルファとオメガです。それがない場合は、宇宙のHOLDLOCK-sによって節約されることはありません。その場合、最初の選択時(または最初に更新を使用するとき)にUPDLOCK以外のものは必要ありません。

Sprocは通常、非常に制御された条件下で実行され、信頼できる呼び出し元(中間層)を想定しています。つまり、単純なアップサートパターン(更新+挿入またはマージ)で重複するPKが検出された場合、中間層またはテーブルの設計にバグがあることを意味し、SQLがそのような場合にエラーを通知してレコードを拒否するのは良いことです。この場合にHOLDLOCKを設定することは、パフォーマンスを低下させることに加えて、例外を食べ、欠陥のある可能性のあるデータを取り込むことと同じです。

そうは言っても、最初に選択するために(UPDLOCK)を追加することを覚えておく必要がないので、MERGEまたはUPDATEを使用してからINSERTを使用すると、サーバーで簡単になり、エラーが発生しにくくなります。また、小さなバッチで挿入/更新を行う場合は、トランザクションが適切かどうかを判断するためにデータを知る必要があります。それは単に関連のないレコードのコレクションであり、追加の「エンベロープ」トランザクションは有害になります。


1
更新を行ってから、ロックや高度な分離を行わずに挿入すると、2人のユーザーが同じデータを返そうとする可能性があります(2人のユーザーがまったく同じ情報を送信しようとした場合、中間層のバグとは見なしません。同時に-状況に大きく依存しますね)。どちらも更新を入力し、どちらも0行を返します。その後、両方が挿入を試みます。1つが勝ち、もう1つは例外を受け取ります。これは、人々が通常避けようとしていることです。
アーロンベルトラン

1

最初に更新に続いて挿入を試みる場合、競合状態は本当に重要ですか?key keyの値を設定したい2つのスレッドがあるとします

スレッド1:値= 1
スレッド2:値= 2

競合状態シナリオの例

  1. キーが定義されていません
  2. スレッド1が更新で失敗する
  3. スレッド2が更新で失敗する
  4. スレッド1またはスレッド2のいずれかが挿入に成功します。たとえば、スレッド1
  5. 他のスレッドは挿入で失敗します(エラー重複キー)-スレッド2。

    • 結果:挿入する2つの踏み板の「最初」が値を決定します。
    • 求められる結果:データを書き込む2つのスレッドの最後(更新または挿入)が値を決定する必要があります

だが; マルチスレッド環境では、OSスケジューラがスレッドの実行順序を決定します。この競合状態がある上記のシナリオでは、実行の順序を決定したのはOSでした。つまり、「スレッド1」または「スレッド2」がシステムの観点から「最初」だったと言うのは間違っています。

スレッド1とスレッド2の実行時間が非常に近い場合、競合状態の結果は重要ではありません。唯一の要件は、スレッドの1つが結果の値を定義することです。

実装の場合:更新に続いて挿入の結果、エラー「重複キー」が発生した場合、これは成功として処理する必要があります。

また、当然のことながら、データベースの値が最後に書き込んだ値と同じであると想定してはいけません。


1

SQL Server 2008では、MERGEステートメントを使用できます


11
これはコメントです。実際のサンプルコードがない場合、これはサイトの他の多くのコメントと同じです。
swasheck 2014年

非常に古いですが、例がいいでしょう。
Matt McCabe

0

以下の解決策を試しましたが、挿入ステートメントの同時要求が発生したときにうまくいきました。

begin tran
if exists (select * from table with (updlock,serializable) where key = @key)
begin
   update table set ...
   where key = @key
end
else
begin
   insert table (key, ...)
   values (@key, ...)
end
commit tran

0

このクエリを使用できます。すべてのSQL Serverエディションで機能します。シンプルで明快です。ただし、2つのクエリを使用する必要があります。MERGEが使えないなら使えます

    BEGIN TRAN

    UPDATE table
    SET Id = @ID, Description = @Description
    WHERE Id = @Id

    INSERT INTO table(Id, Description)
    SELECT @Id, @Description
    WHERE NOT EXISTS (SELECT NULL FROM table WHERE Id = @Id)

    COMMIT TRAN

注:否定的な回答を説明してください


ロックが不足していると思いますか?
Zeek2

ロック不足なし...私は "TRAN"を使用します。デフォルトのsql-serverトランザクションにはロックがあります。
ビクターサンチェス

-2

ADO.NETを使用する場合、DataAdapterがこれを処理します。

あなたがそれを自分で扱いたいなら、これは方法です:

キー列に主キー制約があることを確認してください。

次にあなた:

  1. 更新を行う
  2. キーを持つレコードが既に存在するために更新が失敗した場合は、挿入を実行してください。更新が失敗しない場合は、これで終了です。

逆に行うこともできます。つまり、最初に挿入を行い、挿入が失敗した場合は更新を行います。更新は挿入よりも頻繁に行われるため、通常は最初の方法が適しています。


...そして最初に挿入を行う(時々失敗することを知っている)と、SQL Serverの負荷が高くなります。sqlperformance.com/2012/08/t-sql-queries/error-handling
Aaron Bertrand

-3

ifが存在する... else ...を実行するには、最低2つの要求を実行する必要があります(1つは確認、もう1つはアクションを実行)。次のアプローチでは、レコードが存在する場合は1つだけ必要で、挿入が必要な場合は2つ必要です。

DECLARE @RowExists bit
SET @RowExists = 0
UPDATE MyTable SET DataField1 = 'xxx', @RowExists = 1 WHERE Key = 123
IF @RowExists = 0
  INSERT INTO MyTable (Key, DataField1) VALUES (123, 'xxx')

-3

私は通常、他のポスターのいくつかが言ったことを最初にそれが存在するかどうかを確認し、次に正しいパスが何であれ実行することに関して行います。これを行うときに覚えておかなければならないことの1つは、sqlによってキャッシュされた実行プランがいずれかのパスに対して最適ではない可能性があることです。これを行う最良の方法は、2つの異なるストアドプロシージャを呼び出すことです。

FirstSP:
存在する場合
   SecondSPを呼び出す(UpdateProc)
そうしないと
   ThirdSPを呼び出す(InsertProc)

さて、私は自分のアドバイスにはあまり従わないので、一粒の塩で服用してください。


これはSQL Serverの古いバージョンに関連していた可能性がありますが、最新バージョンにはステートメントレベルのコンパイルがあります。フォークなどは問題ではなく、これらのために別々の手順を使用しても、更新と挿入のどちらかを選択することに固有の問題はいずれにせよ解決されません...
アーロンベルトラン14年

-10

選択を行い、結果が得られたら更新し、そうでなければ作成します。


3
これはデータベースへの2つの呼び出しです。
Chris Cudmore

3
そこに問題はありません。
クリントエッカー

10
問題となるのは、DBへの2つの呼び出しであり、DBへの往復回数を2倍にすることを終了します。アプリが大量の挿入/更新でデータベースにアクセスすると、パフォーマンスが低下します。UPSERTはより良い戦略です。
ケブ

5
また、競合状態を作成しますか?
niico 2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.