テーブル構造をMyTable(KEY, datafield1, datafield2...)
とします。
多くの場合、既存のレコードを更新するか、存在しない場合は新しいレコードを挿入します。
基本的に
IF (key exists)
run update command
ELSE
run insert command
これを書くための最もパフォーマンスの良い方法は何ですか?
取引を忘れないでください。パフォーマンスは良いのですが、単純な(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
私の 非常によく似た以前の質問に対する詳細な回答 を参照してください。
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操作ですが、ひどいコード:-(
UPSERTをします。
MyTable SET UPDATEフィールドA = @ FieldA WHEREキー= @キー IF @@ ROWCOUNT = 0 MyTable(FieldA)値に挿入INSERT IN(@FieldA)[ ]
多くの人があなたが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にキャッチさせる以外の理由ではるかに費用がかかる場合、これは問題になります。私はここでも証明します:
IF EXISTS (SELECT * FROM [Table] WHERE ID = rowID)
UPDATE [Table] SET propertyOne = propOne, property2 . . .
ELSE
INSERT INTO [Table] (propOne, propTwo . . .)
編集:
悲しいかな、私自身の不利益にもかかわらず、私は選択なしでこれを行う解決策がより少ない一歩でタスクを達成するのでよりよいようであることを認めなければなりません。
一度に複数のレコードを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ステートメントの模倣 を調べてください。
これについてはかなり遅れてコメントしていますが、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;
/*
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行の変数に適切な値(および型)を設定します。
乾杯。
MERGE
ステートメントを使用できます。このステートメントは、存在しない場合はデータを挿入し、存在する場合は更新するために使用されます。
MERGE INTO Employee AS e
using EmployeeUpdate AS eu
ON e.EmployeeID = eu.EmployeeID`
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 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を使用することをお勧めします。
それは使用パターンによって異なります。詳細に迷うことなく、使用法の全体像を見なければなりません。たとえば、レコードが作成された後の使用パターンが99%の更新である場合、「UPSERT」が最善の解決策です。
最初の挿入(ヒット)後は、すべての単一ステートメントの更新になります(ifまたはbutsはありません)。そうでなければそれは重複を挿入するでしょう、そしてあなたはロックを扱いたくありません。
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
SQL Server 2008では、MERGEステートメントを使用できます。
MS SQL Server 2008ではMERGEステートメントが導入されました。これはSQL:2003標準の一部であると私は考えています。多くの人が示しているように、単一行のケースを処理することは大したことではありませんが、大規模なデータセットを扱うときは、カーソルが必要です。大規模なデータセットを扱う場合は、MERGEステートメントを追加することを歓迎します。
これらの悪意のあるユーザーがあなたのsprocを直接実行することからの恐怖から皆がHOLDLOCK-sにジャンプする前に、それを指摘させてください設計により新しいPK-sの一意性を保証する必要があります(アイデンティティキー、シーケンスOracleのジェネレータ、外部IDの一意のインデックス、インデックスでカバーされるクエリ)。それが問題のアルファとオメガです。持っていない場合、宇宙のHOLDLOCK-sがあなたを救うことはありません。もし持っているなら、最初の選択でUPDLOCKを超えるものは必要ありません(または最初に更新を使用します)。
通常、Sprocは、非常に制御された条件下で、信頼できる呼び出し元(中間層)を想定して実行されます。つまり、単純なアップサートパターン(更新+挿入またはマージ)で重複PKが検出された場合、中間層またはテーブルデザインのバグを意味し、そのような場合にSQLがエラーを叫んでレコードを拒否するのは良いことです。この場合、HOLDLOCKを設定することは、パフォーマンスを低下させることに加えて、例外を食べて潜在的に障害のあるデータを取り込むことと同じです。
そうは言っても、最初の選択に(UPDLOCK)を追加することを覚えておく必要がないため、サーバーでMERGE、またはUPDATEを使用してINSERTを使用する方が簡単で、エラーが発生しにくくなります。また、小さなバッチで挿入/更新を行う場合は、トランザクションが適切かどうかを判断するためにデータを知る必要があります。それは、無関係なレコードのコレクションに過ぎず、追加の「エンベロープ」トランザクションは有害です。
最初に更新してから挿入を試みる場合、競合状態は本当に重要ですか? key key に値を設定したい2つのスレッドがあるとしましょう。
スレッド1:値= 1
スレッド2:値= 2
競合状態シナリオの例
もう一方のスレッドは(エラー重複キーで)挿入に失敗します - スレッド2。
しかし;マルチスレッド環境では、OSスケジューラがスレッドの実行順序を決定します。このような競合状態が発生する上記のシナリオでは、実行順序を決定したのはOSでした。すなわち、システムの観点からは、「スレッド1」または「スレッド2」が「最初」であったと言うのは間違っています。
実行時間がスレッド1とスレッド2に非常に近い場合、競合状態の結果は重要ではありません。唯一の要件は、スレッドの1つが結果の値を定義することです。
実装のために:更新の後に挿入が続いてエラー "duplicate key"になった場合、これは成功として扱われるべきです。
また、データベース内の値が、最後に書いた値と同じであるとは決して仮定しないでください。
私は解決策を下に試してみましたが、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