2009-07-09 19 views
5

は、私はあなたの基本的なUPSERT機能を実装しようとしているが、ひねりを加えた:時々、私は実際には、既存の行を更新する必要はありません。条件付きUpsertストアドプロシージャを実装する方法は?

基本的に私は、異なるリポジトリの間にいくつかのデータを同期しようとしている、とアップサート機能は、移動するための方法のように思えました。だから、主にSam Saffron's answer to this question、だけでなく、いくつかの他の研究や読書に基づいて、私はこのストアドプロシージャを思い付いた:

(注:私は、MS SQL Server 2005を使用していますので、MERGE文がオプションではありません)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here 
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null, 
    @pTeaser varchar(255) = null 
AS 
BEGIN 
    -- SET NOCOUNT ON added to prevent extra result sets from 
    -- interfering with SELECT statements. 
    SET NOCOUNT ON; 

    BEGIN TRANSACTION 

     UPDATE dbo.Item WITH (SERIALIZABLE) 
     SET Title = @pTitle, 
      Teaser = @pTeaser 
     WHERE ContentID = @pContentID 

     IF @@rowcount = 0 
      INSERT INTO dbo.Item (ContentID, Title, Teaser) 
      VALUES (@pContentID, @pTitle, @pTeaser) 

    COMMIT TRANSACTION 
END 

私は基本的なUp​​sertについてこれに満足していますが、実際の更新を別の列の値で条件付きにしたいと考えています。 Upsertプロシージャによってそれ以上の更新が行われないように行を「ロック」すると考えてください。私はそうのようなUPDATEステートメントを変更することができます:

UPDATE dbo.Item WITH (SERIALIZABLE) 
SET Title = @pTitle, 
    Teaser = @pTeaser 
WHERE ContentID = @pContentID 
AND RowLocked = false 

しかし、それはすでに存在している行を挿入しようとしますが、ので、更新されなかったとき、その後の挿入は、(コンテンツIDフィールドの)ユニーク制約違反で失敗しますそれは "ロックされていた"。

だから、これは私が行にそれを更新または挿入することができるかどうかを判断するためにあらゆる時間を選択する必要がありますこと、すなわち、私はもはや古典アップサートを持っていることを意味しないのでしょうか?私はそれはケースだ賭けているので、私は、私は本当に手順が安全に実行されるようにトランザクション分離レベル正しいヘルプのであるために求めているものを推測します。あなたが更新の順序を切り替えることができ

+0

RowLocked(AND RowLocked = false)とは何ですか?それはあなたのテーブルの列ですか? –

+0

@AlexKuznetsov - はい、RowLockedはテーブルの列になっています。実際には、行を「ロック」(つまりこの手順では更新しない)にするかどうかを指定する列が2つありますが、SQLを簡略化して質問を明確にしています。しかし、文法はちょっとうんざりしています。もちろん、 "AND RowLocked = 0"でなければなりません。 – Matt

答えて

0

/周りに挿入します。したがって、try/catch内で挿入を行い、制約違反が発生した場合は更新を行います。しかし、少し汚い感じ。

+0

私はいつもあなたが「通常の」処理のためのエラーハンドラに頼っていないと思っていました。つまり、典型的なユースケースが例外を発生させることを知っていれば、例外を発生させる前に、 。(私はまだ読んでいます)論理がかなり簡単です - しかし、私はupsertの元の利点を失う(つまり、余分なDBを読んでいない) 。 – Matt

2

私が証拠に、私が過去数年間に使用されるこのトリックを、次のスクリプトを一緒にたたきました。それを使用する場合は、目的に合わせて変更する必要があります。コメントは次のとおりです。

/* 
CREATE TABLE Item 
(
    Title  varchar(255) not null 
    ,Teaser  varchar(255) not null 
    ,ContentId varchar(30) not null 
    ,RowLocked bit not null 
) 


UPDATE item 
set RowLocked = 1 
where ContentId = 'Test01' 

*/ 


DECLARE 
    @Check varchar(30) 
,@pContentID varchar(30) 
,@pTitle varchar(255) 
,@pTeaser varchar(255) 

set @pContentID = 'Test01' 
set @pTitle  = 'TestingTitle' 
set @pTeaser = 'TestingTeasier' 

set @check = null 

UPDATE dbo.Item 
set 
    @Check = ContentId 
    ,Title = @pTitle 
    ,Teaser = @pTeaser 
where ContentID = @pContentID 
    and RowLocked = 0 

print isnull(@check, '<check is null>') 

IF @Check is null 
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked) 
    values (@pContentID, @pTitle, @pTeaser, 0) 

select * from Item 

ここでは、Updateステートメント内のローカル変数に値を設定できるということです。上記の "フラグ"値は、アップデートが機能している場合(つまりアップデート基準が満たされている場合)にのみ設定されます。それ以外の場合は変更されません(ここでは、nullのままです)、それを確認してそれに応じて処理することができます。

トランザクションについては、トランザクションをシリアライズ可能にするために、どのように進めるべきかを提案する前に、トランザクション内にカプセル化されなければならないものについてもっと知りたいと思います。

- 補遺、-----------

あなたの主キーが定義されているので氏サフランのアイデアは、このルーチンを実装するの徹底と固体方法である下記の2番目のコメントからのフォローアップ外に出てデータベースに渡されます(つまり、あなたはIDカラムを使用していません。

もう少しテストを行いました(ContentId列にプライマリキー制約を追加し、トランザクションにUPDATEとINSERTをラップし、シリアル化可能なヒントを更新に追加しました)。更新が失敗すると、インデックスのその部分の範囲ロックが解除され、その列に新しい値を挿入しようとすると同時にブロックされます。もちろん、N個のリクエストが同時に送信された場合は、 "最初の"行が作成され、2番目、3番目などですぐに更新されます。良いトリック!

(キー列のインデックスがないと、テーブル全体がロックされることに注意してください。また、範囲ロックによって、新しい値の「両側」の行がロックされる可能性があります。私はそれをテストしませんでした。操作の期間は[?] 1桁のミリ秒でなければならないので、問題ではありません。

+0

元のサンプルコードでは、テーブルItemを更新しますが、テーブルMailItemに挿入します。同じテーブルに対して適用されるupsertsではありませんか? –

+0

不一致のテーブル名は、タイプミスです(修正されました)。あなたはSELECTを使ってローカル変数を設定できると知っていましたが、UPDATEでそれを試したことはありませんでしたので、ちょっとトリックをするかもしれません。シリアライズ可能なトランザクションに関しては、ある種のロックを使用しないと、一意のキー制約違反が発生し、「シリアライズ可能」のUPDATEによって適切にデッドロックが解除されるという理解は間違いありません。私はリンクされた質問(上記)の例から作業していますが、それでも読んでいる/私はそれが何をしているかを正確に理解していることを確認しようとしています。 – Matt

+0

私の答えは上記のコメントに対するフィードバックで更新されました。 –

8

非常に一般的な問題です。いくつかのアプローチは、高い並行性の下で保持しません。説明とストレスがここでテスト:

Stress testing UPSERTs

Defensive database programming: eliminating IF statements.

をそれだけでいくつかのコードを記述するだけでは十分ではないような場合では、高い同時実行に を公開する必要があります。たとえば、私はCptSkippy の推奨事項を理解しているかどうかはわかりませんが、以下はストレステストの方法を示しています。

CREATE PROCEDURE Testers.UpsertLoop1 
AS 
BEGIN 
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT; 
SET @count = 0; 
WHILE @count<50000 BEGIN 
     SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts; 
    EXEC @ret=dbo.SaveTwoINTs @ID, 1, 0; 
     SET @count = @count + 1; 
END; 
END; 
GO 
CREATE PROCEDURE Testers.UpsertLoop2 
AS 
BEGIN 
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT; 
SET @count = 0; 
WHILE @count<50000 BEGIN 
     SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts; 
    EXEC @ret=dbo.SaveTwoINTs @ID, 0, 1; 
     SET @count = @count + 1; 
END; 
END; 

は二つのタブでこれらの手順を実行して、エラーの多くを得ることを自分の目で確かめてください:

CREATE TABLE [dbo].[TwoINTs](
     [ID] [int] NOT NULL, 
     [i1] [int] NOT NULL, 
     [i2] [int] NOT NULL, 
     [i3] [int] NOT NULL 
); 
CREATE PROCEDURE dbo.SaveTwoINTs(@ID INT, @i1 INT, @i2 INT) 
AS 
BEGIN 
     SET NOCOUNT ON; 
     SET XACT_ABORT OFF; 
     SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 
     DECLARE @ret INT; 
     SET @ret=0; 
     BEGIN TRAN; 
IF EXISTS(SELECT 1 FROM dbo.TwoINTs WHERE [email protected]) BEGIN 
     UPDATE dbo.TwoINTs WITH (SERIALIZABLE) 
     SET [email protected], [email protected] WHERE [email protected]; 
     SET @[email protected]@ERROR; 
END ELSE BEGIN 
    INSERT INTO dbo.TwoINTs(ID, i1, i2, i3)VALUES(@ID, @i1, @i2, @i1); 
     SET @[email protected]@ERROR; 
END; 
COMMIT; 
RETURN @ret; 
END 
GO 

は、そのプロシージャを実行する二つのループを設定します。表と手順を設定

Testers.UpsertLoop1 --run in one tab 
Testers.UpsertLoop1 --run in one tab 

Msg 2601, Level 14, State 1, Procedure SaveTwoINTs, Line 15 
Cannot insert duplicate key row in object 'dbo.TwoINTs' with unique index 'UNQ_TwoInts_ID'. 
The statement has been terminated. 

私が提供したリンクをたどって、実際に並行処理を行う方法を確認してください。

+0

@Alex +1は、テストのストレスを与える方法に関するリンクとアドバイスを提供します。私は間違いなくそれを試してみましょう。 – Matt

0

PROCEDURE [DBO] [usp_UpsertItem] CREATE - 。ここで、ストアドプロシージャのパラメータを追加 @pContentIDのVARCHAR(30)= NULL、 @pTitle VARCHAR(255)= NULL、 @pTeaser VARCHAR(255 )= null AS BEGIN - の余分な結果セットを防ぐためにSET NOCOUNT ONが追加されました。SELECTステートメントに干渉します。 SET NOCOUNT ON;

BEGIN TRANSACTION 
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID 
      AND RowLocked = false) 
     UPDATE dbo.Item 
     SET Title = @pTitle, Teaser = @pTeaser 
     WHERE ContentID = @pContentID 
      AND RowLocked = false 
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID) 
      INSERT INTO dbo.Item (ContentID, Title, Teaser) 
      VALUES (@pContentID, @pTitle, @pTeaser) 

COMMIT TRANSACTION 

END

+2

恐ろしい恐ろしいコード!同じ条件演算に対してクエリを2回実行しているだけでなく、単純な「else」の場合には「else if not exists」を使用しています。より良い例については、CptSkippyの答えを参照してください。 – Chris

+0

私は他の解決策がよりクリーンであることに同意しますが、恐ろしい、ひどい...私は正しい方向を指していましたか? – JNappi

1
BEGIN TRANSACTION 

IF EXISTS(SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID) 
    UPDATE dbo.Item WITH (SERIALIZABLE) 
    SET Title = @pTitle, Teaser = @pTeaser 
    WHERE ContentID = @pContentID 
    AND RowLocked = false 
ELSE 
    INSERT INTO dbo.Item 
      (ContentID, Title, Teaser) 
    VALUES 
      (@pContentID, @pTitle, @pTeaser) 

COMMIT TRANSACTION 
+0

RowLockedとは何ですか(AND RowLocked = false)?それはあなたのテーブルの列ですか? –

+1

私はあなたのアプローチとして私が理解したことをストレステストし、高い並行性を保持しません。 –

-2

私はトランザクションをドロップしたいです。

プラス@@ rowcountはおそらく動作しますが、グローバル変数を条件チェックとして使用するとバグが発生します。

Exists()チェックを行うだけです。とにかくテーブルを通過する必要があるので、スピードは問題ではありません。

私が見る限り、取引は必要ありません。

+1

rowcountを使用した更新/挿入パターンは、直列化可能を使用して挿入までロックするため、安全です。そうしないと、インサートは更新された同時の試行と競合する可能性があり、それも行と一致せず、二重挿入により重複する行が発生したり、ユニークキーがあると重複キーエラーが発生します。 –

関連する問題