条件付きINSERTおよびSELECTよりもOUTPUTを使用したMERGEの方が良いでしょうか?


12

多くの場合、「存在しない場合は挿入」という状況が発生します。Dan Guzmanのブログには、このプロセスをスレッドセーフにする方法に関する優れた調査があります。

文字列をから整数に単純にカタログする基本的なテーブルがありSEQUENCEます。ストアドプロシージャでは、値の整数キーが存在する場合は取得するか、値の整数キーをINSERT取得してから結果の値を取得する必要があります。dbo.NameLookup.ItemName列には一意性の制約があるため、データの整合性は危険にさらされていませんが、例外は発生しません。

それはIDENTITYそうではないので、私は得ることができず、特定の場合にSCOPE_IDENTITYは値がありますNULL

私の状況INSERTでは、テーブルの安全性だけを扱う必要があるため、次のMERGEように使用するのがより良い方法かどうかを判断しようとしています。

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

このウィットアウトMERGEは条件付きでINSERT続けて使用できSELECT ますが、この2番目のアプローチは読者にとってより明確だと思いますが、それが「より良い」実践であるとは確信していません

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

または、おそらく私が考えていない別のより良い方法があります

他の質問を検索して参照しました。これ:https : //stackoverflow.com/questions/5288283/sql-server-insert-if-not-exists-best-practiceは私が見つけることができる最も適切なものですが、私のユースケースにはあまり当てはまらないようです。IF NOT EXISTS() THENアプローチに対する他の質問は受け入れられないと思います。


バッファよりも大きなテーブルを試してみましたが、テーブルが特定のサイズに達するとマージのパフォーマンスが低下するという経験がありました。
pacreely

回答:


8

シーケンスを使用しているので、同じNEXT VALUE FOR関数(Id主キーフィールドの既定の制約に既にある)を使用して、Id事前に新しい値を生成できます。最初に値を生成するということは、を持たないことを心配する必要がないことをSCOPE_IDENTITY意味します。それは、新しい値を取得するためにOUTPUT句や追加SELECTを行う必要がないことを意味します。あなたがする前にあなたは価値を持つでしょうINSERTSET IDENTITY INSERT ON / OFFコードをいじる必要さえありません。

そのため、全体的な状況の一部を処理します。もう1つの部分は、まったく同じ文字列の既存の行を見つけずに、まったく同時に2つのプロセスの同時実行の問題を処理し、INSERTます。懸念事項は、発生するであろう一意の制約違反を回避することです。

これらのタイプの並行性の問題を処理する1つの方法は、この特定の操作を強制的にシングルスレッドにすることです。そのための方法は、アプリケーションロック(セッション間で機能する)を使用することです。効果的ではありますが、衝突の頻度がおそらくかなり低いこのような状況では、それらは少々手荒くなります。

衝突に対処する他の方法は、衝突を回避しようとするのではなく、衝突が時々発生することを受け入れて処理することです。TRY...CATCHコンストラクトを使用すると、特定のエラー(この場合:「一意の制約違反」、メッセージ2601)を効果的にトラップし、その特定のブロックに存在するために値が存在することがわかっているSELECTため、再実行してId値を取得できますCATCHエラー。その他のエラーは、通常のRAISERROR/ RETURNまたはTHROW方法で処理できます。

テストセットアップ:シーケンス、テーブル、および一意のインデックス

USE [tempdb];

CREATE SEQUENCE dbo.MagicNumber
  AS INT
  START WITH 1
  INCREMENT BY 1;

CREATE TABLE dbo.NameLookup
(
  [Id] INT NOT NULL
         CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
        CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
  [ItemName] NVARCHAR(50) NOT NULL         
);

CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
  ON dbo.NameLookup ([ItemName]);
GO

テストセットアップ:ストアドプロシージャ

CREATE PROCEDURE dbo.GetOrInsertName
(
  @SomeName NVARCHAR(50),
  @ID INT OUTPUT,
  @TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;

BEGIN TRY
  SELECT @ID = nl.[Id]
  FROM   dbo.NameLookup nl
  WHERE  nl.[ItemName] = @SomeName
  AND    @TestRaceCondition = 0;

  IF (@ID IS NULL)
  BEGIN
    SET @ID = NEXT VALUE FOR dbo.MagicNumber;

    INSERT INTO dbo.NameLookup ([Id], [ItemName])
    VALUES (@ID, @SomeName);
  END;
END TRY
BEGIN CATCH
  IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
  BEGIN
    SELECT @ID = nl.[Id]
    FROM   dbo.NameLookup nl
    WHERE  nl.[ItemName] = @SomeName;
  END;
  ELSE
  BEGIN
    ;THROW; -- SQL Server 2012 or newer
    /*
    DECLARE @ErrorNumber INT = ERROR_NUMBER(),
            @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();

    RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
    RETURN;
    */
  END;

END CATCH;
GO

テスト

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT,
  @TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO

OPからの質問

なぜこれが良いのMERGEですか?句TRYを使用せずに同じ機能を取得できませんWHERE NOT EXISTSか?

MERGEさまざまな「問題」があります(いくつかの参照は@SqlZimの回答にリンクされているため、ここでその情報を複製する必要はありません)。そして、このアプローチには追加のロックはありません(競合が少ない)ので、並行性のほうが優れているはずです。このアプローチでは、ユニーク制約違反は発生しません。HOLDLOCK。。動作することはほぼ保証されています。

このアプローチの背後にある理由は次のとおりです。

  1. 衝突を心配する必要があるほどこのプロシージャの実行が十分にある場合、次のことはしたくないでしょう。
    1. 必要以上のステップを踏む
    2. 必要以上に長くリソースをロックする
  2. 衝突は新しいエントリ(まったく同じ時間に送信された新しいエントリ)でのみ発生するためCATCH、そもそもブロックに陥る頻度はかなり低くなります。1%の時間を実行するコードではなく、99%の時間を実行するコードを最適化する方が理にかなっています(両方を最適化する費用がない場合を除き、ここではそうではありません)。

@SqlZimの回答からのコメント(強調を追加)

私は個人的には解決策を試して調整し、可能なときにそれを避けることを好みます。この場合、ロックを使用することは手間serializableがかかるとは思わず、高い並行性をうまく処理できると確信しています。

「そして_慎重な」と述べるように修正された場合、この最初の文に同意します。技術的に可能だからといって、その状況(つまり、意図したユースケース)がその恩恵を受けることを意味するわけではありません。

このアプローチで見られる問題は、提案されている以上にロックすることです。「serializable」、特に以下の強調されたドキュメントを再読することが重要です:

  • 他のトランザクションは、現在のトランザクションが完了するまで、現在のトランザクションのステートメントによって読み取られたキー範囲に収まるキー値を持つ新しい行を挿入できません。

次に、サンプルコードのコメントを示します。

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

そこに有効な言葉は「範囲」です。取得されるロックは、の値だけ@vNameでなく、より正確には範囲ですこの新しい値が移動する場所(つまり、新しい値が収まる場所の両側にある既存のキー値の間)。ただし、値自体ではありません。つまり、現在検索されている値に応じて、他のプロセスは新しい値を挿入できなくなります。ルックアップが範囲の最上部で行われている場合、同じ位置を占める可能性のあるものを挿入することはブロックされます。たとえば、値 "a"、 "b"、および "d"が存在し、1つのプロセスが "f"でSELECTを実行している場合、値 "g"または "e"を挿入することはできません(それらのいずれかが「d」の直後に来るため)。ただし、「予約済み」の範囲に配置されないため、「c」の値を挿入することは可能です。

次の例でこの動作を説明します。

(クエリタブ(つまりセッション)#1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');

BEGIN TRAN;

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'test8';

--ROLLBACK;

(クエリタブ(セッション)#2で)

EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

同様に、値「C」が存在し、値「A」が選択されている(したがってロックされている)場合、「D」の値を挿入できますが、「B」の値は挿入できません。

(クエリタブ(つまりセッション)#1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');

BEGIN TRAN

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'testA';

--ROLLBACK;

(クエリタブ(セッション)#2で)

EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

公平を期すために、提案されたアプローチでは、例外がある場合、この「シリアル化可能なトランザクション」アプローチでは発生しないトランザクションログに4つのエントリがあります。ただし、前述したように、例外が1%(または5%)発生する場合、最初のSELECTが一時的にINSERT操作をブロックする可能性がはるかに高い場合よりも、影響ははるかに少なくなります。

この「シリアル化可能なトランザクション+ OUTPUT句」アプローチのもう1つのマイナーな問題は、そのOUTPUT句(現在の使用法では)がデータを結果セットとして送り返すことです。結果セットには、単純なOUTPUTパラメーターよりも多くのオーバーヘッドが必要です(おそらく、両側:内部カーソルを管理するSQL Serverと、DataReaderオブジェクトを管理するアプリ層)。単一のスカラー値のみを処理しており、実行の頻度が高いと仮定すると、結果セットの余分なオーバーヘッドはおそらく加算されます。

このOUTPUT句は、OUTPUTパラメータを返すような方法で使用できますが、一時テーブルまたはテーブル変数を作成し、その一時テーブル/テーブル変数からOUTPUTパラメータに値を選択する追加の手順が必要になります。

さらなる明確化:同時性とパフォーマンスに関するステートメントに対する@SqlZimの応答(元の回答)への@SqlZimの応答(更新された回答)への応答;-)

この部分がほんの少しの長さの場合は申し訳ありませんが、この時点では2つのアプローチのニュアンスにかかっています。

情報の表示方法はserializable、元の質問で示したシナリオで使用するときに発生する可能性のあるロックの量に関する誤った仮定につながる可能性があると思います。

はい、公平であるとはいえ、偏見があることは認めます。

  1. 人間が偏らないようにすることは、少なくともある程度は不可能であり、私はそれを最小限に抑えようとしていますが、
  2. 与えられた例は単純化されていますが、それは説明を目的としており、動作を過度に複雑にすることなく伝えています。過度の頻度を意味することは意図していませんでしたが、私は明示的に別のことを述べておらず、実際に存在するよりも大きな問題を暗示するように読めることを理解しています。以下でそれを明確にしようとします。
  3. また、2つの既存のキー(「クエリタブ1」ブロックと「クエリタブ2」ブロックの2番目のセット)間の範囲をロックする例を含めました。
  4. 私は、アプローチの「隠れたコスト」を見つけました(そしてボランティアしました)INSERT。これは、ユニーク制約違反が原因で失敗するたびに追加の4つのTran Logエントリでした。私は他の回答/投稿のいずれにも言及されていません。

@gbnの「JFDI」アプローチ、Michael J. Swartの「Ugly Pragmatism For The Win」投稿、およびAaron BertrandのMichaelの投稿に対するコメント(パフォーマンスが低下したシナリオを示す彼のテストに関して)、および「Michael Jの適応に関するコメント」 。@gbnのTry Catch JFDI手順のスチュワートの適応」

既存の値を選択するよりも頻繁に新しい値を挿入する場合は、@ srutzkyのバージョンよりもパフォーマンスが向上する可能性があります。そうでなければ、私はこのバージョンより@srutzkyのバージョンを好むでしょう。

「JFDI」アプローチに関連するそのGBN /マイケル/アーロンの議論に関して、GBNの「JFDI」アプローチと私の提案を同一視するのは間違っています。「取得または挿入」操作の性質により、既存のレコードの値SELECTを取得するために明示的に行う必要がありIDます。このSELECTはIF EXISTSチェックとして機能するため、このアプローチは、Aaronのテストの「CheckTryCatch」バリエーションと同等になります。Michaelの書き直されたコード(およびMichaelの適応の最終的な適応)には、WHERE NOT EXISTS最初に同じチェックを行うことも含まれています。したがって、私の提案(Michaelの最終コードと彼の最終コードの適応とともに)は、実際にはそれほどCATCH頻繁にはブロックにヒットしません。2つのセッション、ItemNameINSERT...SELECTまったく同じ瞬間に、両方のセッションがまったく同じ瞬間に「true」を受信するため、両方WHERE NOT EXISTSがまったく同じ瞬間に実行しようとしますINSERT。その非常に特定のシナリオは、他のプロセスがまったく同じ瞬間にそうしようとしていないとき、既存を選択するItemNameか新しいを挿入するよりはるかに少ない頻度で起こります。ItemName

上記のすべての場合:なぜ自分のアプローチを好むのですか?

まず、「シリアライズ可能」アプローチでロックが行われることを見てみましょう。前述のように、ロックされる「範囲」は、新しいキー値が収まる場所の両側の既存のキー値によって異なります。その方向に既存のキー値がない場合、範囲の開始または終了は、それぞれインデックスの開始または終了にもなります。次のインデックスとキーがあると仮定します(インデックス^の始まりを$表し、インデックスの終わりを表します)。

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

セッション55が次のキー値を挿入しようとした場合:

  • A、その後、範囲#1(^C)がロックされます:セッション56はB、一意で有効な(まだ)場合でも、値を挿入できません。ただし、セッション56 Dでは、、、Gおよびの値を挿入できますM
  • D、次いで(#2からの範囲CにはFロックされている):セッション56の値を挿入することができないE(がまだ)。ただし、セッション56 Aでは、、、Gおよびの値を挿入できますM
  • M、次いで(から#4の範囲Jには$ロックされている):セッション56の値を挿入することができないX(がまだ)。ただし、セッション56 Aでは、、、Dおよびの値を挿入できますG

キー値が追加されると、キー値間の範囲が狭くなるため、同じ範囲で複数の値が同時に挿入される確率/頻度が低下します。確かに、これは大きな問題ではなく、幸いことに、時間の経過とともに実際に減少する問題のようです。

私のアプローチの問題は上記のとおりです。2つのセッションが同じキー値を同時に挿入しようとした場合にのみ発生します。この点で、発生する可能性が高いものに帰着します。2つの異なる、しかし近いキー値が同時に試行されるか、同じキー値が同時に試行されますか?答えは挿入を行うアプリの構造にあると思いますが、一般的に言えば、たまたま同じ範囲を共有する2つの異なる値が挿入される可能性が高いと想定します。しかし、実際に知る唯一の方法は、OPシステムで両方をテストすることです。

次に、2つのシナリオと、各アプローチがそれらをどのように処理するかを考えてみましょう。

  1. 一意のキー値に対するすべてのリクエスト:

    この場合、CATCH私の提案のブロックは入力されないため、「問題」は発生しません(つまり、4つのtranログエントリとその実行にかかる時間)。しかし、「シリアライズ可能」アプローチでは、すべての挿入が一意であっても、同じ範囲内の他の挿入をブロックする可能性が常にあります(非常に長い間ではありませんが)。

  2. 同時に同じキー値を要求する頻度が高い:

    この場合-存在しないキー値に対する着信要求に関して非常に低い一意性- CATCH私の提案のブロックは定期的に入力されます。この結果、挿入に失敗するたびに、自動ロールバックが必要になり、4つのエントリがトランザクションログに書き込まれます。これは、毎回わずかなパフォーマンスヒットになります。ただし、操作全体が失敗することはありません(少なくともこれが原因ではありません)。

    (以前のバージョンの「更新された」アプローチには、デッドロックに悩まされる問題がありました。updlockこれに対処するためのヒントが追加され、デッドロックが発生しなくなりました。)しかし、「シリアライズ可能な」アプローチでは(更新され、最適化されたバージョンでも)、操作はデッドロックします。どうして?このserializable動作INSERTは、読み取られてロックされた範囲内の操作のみを防止するためです。SELECTその範囲での操作を妨げません。

    serializableこの場合のアプローチは、追加のオーバーヘッドがないように思われ、私が提案しているよりもわずかに優れたパフォーマンスを発揮する可能性があります。

パフォーマンスに関する多くの/ほとんどの議論と同様に、結果に影響を与える可能性のある要因が非常に多いため、何かがどのように実行されるかを実際に把握する唯一の方法は、実行されるターゲット環境でそれを試すことです。その時点では意見の問題ではありません:)。


7

更新された回答


@srutzkyへの応答

この「直列化可能なトランザクション+ OUTPUT句」アプローチのもう1つのマイナーな問題は、OUTPUT句(現在の使用法では)が結果セットとしてデータを送り返すことです。結果セットには、単純なOUTPUTパラメーターよりも多くのオーバーヘッドが必要です(おそらく、両側:内部カーソルを管理するSQL Serverと、DataReaderオブジェクトを管理するアプリ層)。単一のスカラー値のみを処理しており、実行の頻度が高いと仮定すると、結果セットの余分なオーバーヘッドはおそらく加算されます。

同意しますが、同じ理由で慎重に出力パラメーターを使用します。最初の回答で出力パラメーターを使用しなかったのは私の間違いで、怠けていました。

ここで一緒に出力パラメータを使用して改訂された手順、追加の最適化、であるnext value forことを@srutzkyが彼の答えで説明しては

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

注の更新updlockselectを含めると、このシナリオで適切なロックが取得されます。のみ使用している場合、これはデッドロックを引き起こす可能性があると指摘し@srutzkyのおかげで、serializableselect

注:このかもしれないがそうではないが、それが可能な場合の手順は、のための値で呼び出される@vValueId、などがset @vValueId = null;後にset xact_abort on;そうでない場合は削除することができ、。


キー範囲ロック動作の@srutzkyの例に関して:

@srutzkyは彼のテーブルで1つの値のみを使用し、キー範囲のロックを示すために彼のテストで「次」/「無限」キーをロックします。彼のテストはそのような状況で何が起こるかを示していますが、情報が提示される方法はserializable、元の質問で提示されたシナリオで使用するときに遭遇する可能性のあるロックの量に関する誤った仮定につながる可能性があると思います。

私が彼が彼の説明とキー範囲ロックの例を提示する方法に偏りを(おそらく誤って)知覚しているとしても、それらはまだ正しい。


さらに調査を重ねた結果、2011年のMichael J. Swartによる特に適切なブログ記事が見つかりました:Mythbusting:Concurrent Update / Insert Solutions。その中で、彼は複数のメソッドの正確性と同時実行性をテストしています。方法4:分離の増加+ロックの微調整は、Sam SaffronのSQL Serverの挿入または更新後のパターンに基づいており、元のテストで彼の期待を満たす唯一の方法です(後で参加しますmerge with (holdlock))。

2016年2月、Michael J. SwartがWinのismいプラグマティズムを投稿しました。その投稿では、ロックを減らすために、彼がサフランのアップサート手順に対して行った追加のチューニングについて説明しています(これは上記の手順に含まれています)。

これらの変更を行った後、マイケルは自分の手順がより複雑に見え始めたことに満足せず、クリスという名前の同僚に相談しました。クリスは、Mythbustersの最初の投稿をすべて読み、コメントをすべて読んで、@ gbnの TRY CATCH JFDIパターンについて尋ねました。このパターンは@srutzkyの答えに似ており、マイケルがそのインスタンスで使用することになった解決策です。

マイケル・J・スワート:

昨日、並行性を実現する最善の方法について考えが変わりました。Mythbusting:Concurrent Update / Insert Solutionsでいくつかの方法を説明しています。私の好ましい方法は、分離レベルを上げてロックを微調整することです。

少なくともそれは私の好みでした。最近、コメントで提案されたgbnのメソッドを使用するようにアプローチを変更しました。彼は自分の方法を「TRY CATCH JFDIパターン」と説明しています。通常、私はそのような解決策を避けます。開発者は、制御フローのエラーや例外をキャッチすることに依存すべきではないという経験則があります。しかし、私は昨日その経験則を破りました。

ところで、パターン "JFDI"のgbnの説明が大好きです。シーア・ラブーフのやる気を起こさせるビデオを思い出します。


私の意見では、どちらのソリューションも実行可能です。私は依然として分離レベルを上げてロックを微調整することを好みますが、@ srutzkyの答えも有効であり、特定の状況ではよりパフォーマンスが高い場合とない場合があります。

おそらく将来的には、Michael J. Swartと同じ結論にたどり着くかもしれませんが、まだそこにはいません。


私の好みではありませんが、Michael J. Stewartの@gbnのTry Catch JFDI手順の適応は次のようになります。

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

既存の値を選択するよりも頻繁に新しい値を挿入する場合、これは@srutzkyのバージョンよりもパフォーマンスが高い場合があります。そうでなければ、私はこのバージョンより@srutzkyのバージョンを好むでしょう。

Michael J Swartの投稿に対するAaron Bertrandのコメントは、彼が行ってこの交換につながった関連テストにリンクしています。glyいプラグマティズムの勝利に関するコメントセクションからの抜粋:

ただし、失敗した呼び出しの割合に応じて、JFDIが全体的なパフォーマンスを低下させることがあります。例外の発生にはかなりのオーバーヘッドがあります。これをいくつかの投稿で示しました。

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

アーロン・バートランドによるコメント— 2016年2月11日午前11時49分

および返信:

あなたは正しいアーロンです、そして私たちはそれをテストしました。

このケースでは、失敗したコールの割合は0でした(最も近い割合に丸めた場合)。

可能な限り、経験則に従うよりもケースバイケースで物事を評価するという点を説明すると思います。

厳密に必要ではないWHERE NOT EXISTS句を追加した理由でもあります。

Michael J. Swartによるコメント— 2016年2月11日午前11時57分


新しいリンク:


元の答え


私は、特に単一の行を扱う場合、Sam Saffronのアップサートアプローチを使用するよりも好んでいmergeます。

次のように、このアップサートメソッドをこの状況に適応させます。

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

私はあなたの命名と一貫性serializableがあり、と同じようにholdlock、1つを選択し、その使用において一貫性があります。serializable指定するときと同じ名前であるため、使用する傾向がありset transaction isolation level serializableます。

を使用するserializableholdlock、値に基づいて範囲ロックが取得され、その値に基づいて値が@vName選択または挿入された場合、他の操作は待機dbo.NameLookupしますwhere

範囲ロックが適切に機能するためにItemNameは、使用するときにmergeも適用される列にインデックスが必要です。


ここでの手順は次のようになります。主に以下のエラー処理にErland Sommarskogのホワイトペーパーを使用して、throwthrowエラーの発生方法がそうでない場合は、残りの手順と一致するように変更します。

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

上記の手順で何が起こっているのかをまとめると、 set nocount on; set xact_abort on;いつものように、入力変数is nullまたは空の場合select id = cast(null as int)、結果として。nullまたは空でない場合はId、変数がない場合に備えてそのスポット保持しながら変数を取得します。がある場合Idは、送信します。存在しない場合は、挿入してそのnewを送信しIdます。

一方、同じ値のIDを見つけようとするこのプロシージャへの他の呼び出しは、最初のトランザクションが完了するまで待機してから、それを選択して返します。このプロシージャに対する他の呼び出し、または他の値を探す他のステートメントは、これが邪魔にならないため続行されます。

衝突を処理し、この種の問題の例外を飲み込むことができるという@srutzkyには同意しますが、個人的には可能な限りそれを避けるために解決策を試して調整することを好みます。この場合、ロックを使用するのは手間serializableがかかるアプローチだとは思わず、高い並行性をうまく処理できると確信しています。

テーブルヒントに関するSQLサーバードキュメントserializableholdlockからの引用/

シリアライズ可能

HOLDLOCKと同等です。トランザクションが完了したかどうかにかかわらず、必要なテーブルまたはデータページが不要になり次第共有ロックを解放するのではなく、トランザクションが完了するまで共有ロックを保持することにより、共有ロックをより制限します。スキャンは、SERIALIZABLE分離レベルで実行されているトランザクションと同じセマンティクスで実行されます。分離レベルの詳細については、「SET TRANSACTION ISOLATION LEVEL(Transact-SQL)」を参照してください。

トランザクション分離レベルに関するSQLサーバーのドキュメントからの引用serializable

SERIALIZABLE次を指定します。

  • ステートメントは、変更されたが他のトランザクションによってまだコミットされていないデータを読み取ることはできません。

  • 他のトランザクションは、現在のトランザクションが完了するまで、現在のトランザクションによって読み取られたデータを変更できません。

  • 他のトランザクションは、現在のトランザクションが完了するまで、現在のトランザクションのステートメントによって読み取られたキーの範囲に収まるキー値を持つ新しい行を挿入できません。


上記のソリューションに関連するリンク:

MERGEむらのある歴史があり、その構文の下でコードが意図したとおりに動作していることを確認するために、さらに突っ込んでいるようです。関連merge記事:

最後のリンク、Kendra Littlevsの大まかな比較をmergeinsert with left join行いましたが、「これについて徹底的な負荷テストを行っていません」と警告していますが、それでも良い読み物です。

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