web-dev-qa-db-ja.com

行がまだない場合にのみ挿入する

私はそれを達成するために常に次のようなものを使用していました。

INSERT INTO TheTable
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WHERE
        PrimaryKey = @primaryKey)

...しかし、負荷がかかると、主キー違反が発生しました。これは、このテーブルにまったく挿入する唯一のステートメントです。これは、上記のステートメントがアトミックではないことを意味しますか?

問題は、これを自由に再現することはほとんど不可能であることです。

おそらく次のように変更できます。

INSERT INTO TheTable
WITH
    (HOLDLOCK,
    UPDLOCK,
    ROWLOCK)
SELECT
    @primaryKey,
    @value1,
    @value2
WHERE
    NOT EXISTS
    (SELECT
        NULL
    FROM
        TheTable
    WITH
        (HOLDLOCK,
        UPDLOCK,
        ROWLOCK)
    WHERE
        PrimaryKey = @primaryKey)

ただし、間違ったロックを使用している、またはロックなどを使用しすぎている可能性があります。

Stackoverflow.comで「IF(SELECT COUNT(*)... INSERT」など)が提案されている他の質問を見てきましたが、単一のSQLステートメントがアトミックであるという(おそらく誤った)仮定の下に常にいました。

誰にもアイデアはありますか?

66
Adam

"JFDI" パターンはどうですか?

_BEGIN TRY
   INSERT etc
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() <> 2627
      RAISERROR etc
END CATCH
_

真剣に、これは、特に大容量の場合、ロックなしで最も速く、最も同時です。 UPDLOCKがエスカレートされ、テーブル全体がロックされた場合はどうなりますか?

レッスン4を読む

レッスン4:インデックスを調整する前にupsertプロシージャを開発するとき、最初にIf Exists(Select…)行がすべてのアイテムに対して起動することを信頼しました重複を禁止します。 N。同じアイテムが同じミリ秒でアップサートにヒットし、両方のトランザクションが存在しないことを確認して挿入を実行するため、短時間で何千もの重複がありました。ソリューションをテストした後、一意のインデックスを使用してエラーをキャッチし、トランザクションが行を参照して挿入ではなく更新を実行できるように再試行しました。

57
gbn

もともとは存在していなかったHOLDLOCKを追加しました。このヒントのないバージョンは無視してください。

私の知る限り、これで十分です:

INSERT INTO TheTable 
SELECT 
    @primaryKey, 
    @value1, 
    @value2 
WHERE 
    NOT EXISTS 
    (SELECT 0
     FROM TheTable WITH (UPDLOCK, HOLDLOCK)
     WHERE PrimaryKey = @primaryKey) 

また、行が存在する場合は実際に更新し、存在しない場合は挿入する場合は、 この質問 が役立つ場合があります。

23
GSerg

MERGEを使用できます:

MERGE INTO Target
USING (VALUES (@primaryKey, @value1, @value2)) Source (key, value1, value2)
ON Target.key = Source.key
WHEN MATCHED THEN
    UPDATE SET value1 = Source.value1, value2 = Source.value2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Name, ReasonType) VALUES (@primaryKey, @value1, @value2)
17
Chris Smith

これが「公式」な方法かどうかはわかりませんが、INSERTを試してみて、失敗した場合はUPDATEにフォールバックできます。

1
Marcelo Cantos

まず、コミュニティへの貢献について、@ gbnに大声で叫んでください。彼のアドバイスに従っている頻度を説明することさえできません。

とにかく、十分なファンボイイング。

彼の答えにわずかに追加するために、おそらくそれを「強化」します。私のような人たちにとって、<> 2627シナリオ(空のCATCHはオプションではありません)。 technet からこの小さなナゲットを見つけました。

    BEGIN TRY
       INSERT etc
    END TRY
    BEGIN CATCH
        IF ERROR_NUMBER() <> 2627
          BEGIN
                DECLARE @ErrorMessage NVARCHAR(4000);
                DECLARE @ErrorSeverity INT;
                DECLARE @ErrorState INT;

                SELECT @ErrorMessage = ERROR_MESSAGE(),
                @ErrorSeverity = ERROR_SEVERITY(),
                @ErrorState = ERROR_STATE();

                    RAISERROR (
                        @ErrorMessage,
                        @ErrorSeverity,
                        @ErrorState
                    );
          END
    END CATCH
1
pim