SQL Server での INSERT または UPDATE のソリューション
-
01-07-2019 - |
質問
次のテーブル構造を想定します。 MyTable(KEY, datafield1, datafield2...)
.
多くの場合、既存のレコードを更新するか、新しいレコードが存在しない場合は挿入する必要があります。
基本的に:
IF (key exists)
run update command
ELSE
run insert command
これを書くのに最もパフォーマンスの高い方法は何でしょうか?
解決
取引を忘れないでください。パフォーマンスは良好ですが、単純な (IF EXISTS..) アプローチは非常に危険です。
複数のスレッドが挿入またはアップデートを実行しようとする場合、主要な違反を簡単に取得できます。
@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 以前では良い方法ですが、担当者を付与する場合は、 最初にSOを実行した男. 。唯一の問題は、挿入の場合、依然として 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 操作だけですが、ひどいコードです :-(
UPSERT を実行します。
UPDATE MyTable SET FieldA=@FieldA WHERE Key=@Key IF @@ROWCOUNT = 0 INSERT INTO MyTable (FieldA) VALUES (@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 回読み取る必要がある場合があることだけです。最初のサンプルでは、行を 1 回だけ見つける必要があります。(どちらの場合も、最初の読み取りで行が見つからなかった場合は、挿入が発生します。)
他の人は次のように提案するでしょう。
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 . . .)
編集:
悲しいことに、私自身の不利益にもなりますが、選択を行わずにこれを行うソリューションの方が、1 つ少ないステップでタスクを完了できるため、より優れているように思えます。
一度に複数のレコードを 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
テーブル名とフィールド名を必要なものに置き換えます。世話をしてください 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 を使用することをお勧めします。
SQL Server 2008 では、MERGE ステートメントを使用できます。
それは使用パターンによって異なります。細部に惑わされることなく、使用状況の全体像を把握する必要があります。たとえば、レコードの作成後に 99% が更新される使用パターンの場合、「UPSERT」が最適なソリューションです。
最初の挿入 (ヒット) の後は、すべて単一ステートメントの更新となり、if や but は行われません。挿入時に「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
MS SQL Server 2008 では、SQL:2003 標準の一部である MERGE ステートメントが導入されています。多くの人が示しているように、1 行のケースを処理するのは大したことではありませんが、大規模なデータセットを処理する場合はカーソルが必要になり、パフォーマンス上の問題がすべて発生します。MERGE ステートメントは、大規模なデータセットを扱う場合に非常に歓迎される追加です。
sproc を直接実行する悪意のあるユーザーを恐れて、誰もが HOLDLOCK に飛びつく前に :-) 指摘しておきます。 新しい PK の一意性を設計により保証する必要があります (ID キー、Oracle のシーケンス ジェネレーター、外部 ID の一意のインデックス、インデックスでカバーされるクエリ)。それがこの問題のアルファとオメガです。それを持っていない場合は、世界中のどの HOLDLOCK もあなたを救ってくれません。もしそれを持っているなら、最初の選択 (または最初に更新を使用する) で UPDLOCK 以外は何も必要ありません。
通常、Sproc は非常に制御された条件下で、信頼できる呼び出し元 (中間層) を想定して実行されます。つまり、単純な更新/挿入パターン (更新 + 挿入またはマージ) で重複した PK が見つかった場合、それは中間層またはテーブルの設計にバグがあることを意味し、そのような場合に SQL がエラーを叫び、レコードを拒否するのは良いことです。この場合に HOLDLOCK を設定すると、パフォーマンスが低下するだけでなく、例外が発生し、潜在的に欠陥のあるデータが取り込まれることになります。
そうは言っても、MERGE または UPDATE から INSERT を使用すると、最初に選択するために (UPDLOCK) を追加することを覚えておく必要がないため、サーバーでの処理が簡単になり、エラーが発生しにくくなります。また、小さなバッチで挿入/更新を実行している場合は、トランザクションが適切かどうかを判断するためにデータを知る必要があります。それが無関係なレコードの単なるコレクションである場合、追加の「エンベロープ」トランザクションは有害になります。
最初に更新を試みてから挿入を試みた場合、競合状態は本当に重要ですか?キーの値を設定したいスレッドが 2 つあるとします。 鍵:
スレッド 1:値 = 1
スレッド 2:値 = 2
競合状態シナリオの例
- 鍵 定義されていません
- スレッド 1 の更新が失敗する
- スレッド 2 の更新が失敗する
- スレッド 1 またはスレッド 2 の正確に 1 つが挿入に成功します。例えば。スレッド1
もう一方のスレッドは挿入で失敗します (重複キーエラーが発生) - スレッド 2。
- 結果:挿入する 2 つのトレッドの「最初」が値を決定します。
- 求められる結果:データを書き込む 2 つのスレッド (更新または挿入) の最後のスレッドが値を決定する必要があります。
しかし;マルチスレッド環境では、OS スケジューラがスレッドの実行順序を決定します。上記のシナリオでは、この競合状態が発生しており、実行順序を決定したのは OS でした。つまり:システムの観点から、「スレッド 1」または「スレッド 2」が「最初」であると言うのは間違いです。
スレッド 1 とスレッド 2 の実行時間が非常に近い場合、競合状態の結果は問題になりません。唯一の要件は、スレッドの 1 つが結果の値を定義することです。
実装の場合:更新に続いて挿入の結果「キーの重複」エラーが発生した場合、これは成功として扱われます。
また、もちろん、データベース内の値が最後に書き込んだ値と同じであると想定してはなりません。
私は以下の解決策を試しましたが、挿入ステートメントの同時リクエストが発生したときにうまくいきました。
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
このクエリを使用できます。すべての 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
注記:否定的な答えを説明してください
ADO.NET を使用する場合、DataAdapter がこれを処理します。
自分で処理したい場合は、次の方法があります。
キー列に主キー制約があることを確認してください。
それからあなた:
- アップデートを実行する
- キーを持つレコードが既に存在するために更新が失敗した場合は、挿入を実行します。アップデートが失敗しなければ、完了です。
逆のこともできます。つまり、最初に挿入を実行し、挿入が失敗した場合は更新を実行します。更新は挿入よりも頻繁に行われるため、通常は最初の方法の方が適しています。
if が存在する場合を実行しています...それ以外 ...少なくとも 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')
私は通常、最初にそれが存在するかどうかを確認してから、正しいパスであることを実行することに関して他の投稿者の何人かが言ったことを実行します。これを行う際に覚えておくべきことの 1 つは、SQL によってキャッシュされた実行プランが、どちらかのパスに対して最適ではない可能性があるということです。これを行う最善の方法は、2 つの異なるストアド プロシージャを呼び出すことだと思います。
FirstSP: If Exists Call SecondSP (UpdateProc) Else Call ThirdSP (InsertProc)
さて、私は自分自身のアドバイスにあまり従わないので、話半分に聞いてください。
選択を実行し、結果が得られた場合は更新し、そうでない場合は作成します。