web-dev-qa-db-ja.com

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

テーブル構造をMyTable(KEY, datafield1, datafield2...)とします。

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

基本的に

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

これを書くための最もパフォーマンスの良い方法は何ですか?

535
Chris Cudmore

取引を忘れないでください。パフォーマンスは良いのですが、単純な(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
336
aku

私の 非常によく似た以前の質問に対する詳細な回答 を参照してください。

SQL 2005以下では @Beau Crawford's が良い方法ですが、repを与えているのであれば 最初のSO it に行くべきです。唯一の問題は、挿入に対してそれがまだ2つのIO操作であるということです。

MS Sql2008では、SQL:2003規格のmergeが導入されています。

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, ... )

今では本当にただ一つのIO操作ですが、ひどいコード:-(

367
Keith

UPSERTをします。

 MyTable SET UPDATEフィールドA = @ FieldA WHEREキー= @キー
 IF @@ ROWCOUNT = 0 
 MyTable(FieldA)値に挿入INSERT IN(@FieldA)[ ]

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

155
Beau Crawford

多くの人があなたが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にキャッチさせる以外の理由ではるかに費用がかかる場合、これは問題になります。私はここでも証明します:

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

編集:

悲しいかな、私自身の不利益にもかかわらず、私は選択なしでこれを行う解決策がより少ない一歩でタスクを達成するのでよりよいようであることを認めなければなりません。

51
Esteban Araya

一度に複数のレコードを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ステートメントの模倣 を調べてください。

36
Eric Weilnau

これについてはかなり遅れてコメントしていますが、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;
10
user243131
/*
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

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

乾杯。

8
Denver

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

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

UPDATE if-no-rows-updatedを実行した後に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 [email protected]
   WHERE [email protected]
   IF @@ROWCOUNT = 0
   ... record was deleted, consider looping to re-run the INSERT, or RAISERROR ...
END

競合状態を避けることとは別に、ほとんどの場合、レコードがすでに存在していると、INSERTが失敗し、CPUが無駄になります。

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

4
Kristen

それは使用パターンによって異なります。詳細に迷うことなく、使用法の全体像を見なければなりません。たとえば、レコードが作成された後の使用パターンが99%の更新である場合、「UPSERT」が最善の解決策です。

最初の挿入(ヒット)後は、すべての単一ステートメントの更新になります(ifまたはbutsはありません)。そうでなければそれは重複を挿入するでしょう、そしてあなたはロックを扱いたくありません。

UPDATE <tableName> SET <field>[email protected] WHERE [email protected];

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

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

3
Bart

MS SQL Server 2008ではMERGEステートメントが導入されました。これはSQL:2003標準の一部であると私は考えています。多くの人が示しているように、単一行のケースを処理することは大したことではありませんが、大規模なデータセットを扱うときは、カーソルが必要です。大規模なデータセットを扱う場合は、MERGEステートメントを追加することを歓迎します。

2
bjorsig

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

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

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

1
ZXX

最初に更新してから挿入を試みる場合、競合状態は本当に重要ですか? 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つが結果の値を定義することです。

実装のために:更新の後に挿入が続いてエラー "duplicate key"になった場合、これは成功として扱われるべきです。

また、データベース内の値が、最後に書いた値と同じであるとは決して仮定しないでください。

1
runec

私は解決策を下に試してみましたが、insert文の同時リクエストが発生したとき、それは私のために働きます。

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
Dev