マージステートメント自体のデッドロック


22

次の手順があります(SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId、UserId、MyKeyは、ターゲットテーブルの複合キーを形成します。CompanyIdは、親テーブルへの外部キーです。また、に非クラスター化インデックスがありCompanyId asc, UserId ascます。

多くの異なるスレッドから呼び出され、この同じステートメントを呼び出している異なるプロセス間で一貫してデッドロックを取得しています。私の理解では、「with(holdlock)」は挿入/更新の競合状態エラーを防ぐために必要でした。

2つの異なるスレッドが制約を検証しているときに行(またはページ)を異なる順序でロックしているため、デッドロックしていると想定します。

これは正しい仮定ですか?

この状況を解決する最良の方法は何ですか(つまり、デッドロックなし、マルチスレッドパフォーマンスへの最小の影響)。

クエリプランイメージ (画像を新しいタブで表示する場合、画像は判読可能です。小さいサイズで申し訳ありません。)

  • @datatableには最大28行があります。
  • 私はコードをさかのぼって追跡しましたが、ここでトランザクションを開始することはどこにもわかりません。
  • 外部キーは、削除時にのみカスケードするように設定されており、親テーブルからの削除はありませんでした。

回答:


12

OK、何回かすべてを見て、基本的な仮定は正しかったと思います。おそらくここで起こっているのはそれです:

  1. MERGEのMATCH部分は、一致するインデックスをチェックし、それらの行/ページを読み取りロックします。

  2. 一致する行がない場合、最初に新しいインデックス行を挿入しようとするため、行/ページの書き込みロックが要求されます...

しかし、別のユーザーが同じ行/ページでステップ1に進んだ場合、最初のユーザーは更新からブロックされ、...

2番目のユーザーも同じページに挿入する必要がある場合、デッドロック状態にあります。

私の知る限り、この手順でデッドロックを取得できないことを100%確実にする(簡単な)方法は1つしかありません。これは、TABLOCKXヒントをMERGEに追加することですが、それはおそらくパフォーマンスに本当に悪い影響を与えます。

可能性の代わりにTABLOCKヒントを追加すると、大きなにあなたのパフォーマンスに影響を与えることなく、問題を解決するのに十分だろうと。

最後に、PAGLOCK、XLOCK、またはPAGLOCKとXLOCKの両方を追加してみることもできます。再びそのかもしれないの仕事とパフォーマンスがありますあまりにもひどいことはありません。確認するには試してみる必要があります。


ここでスナップショット分離レベル(行のバージョン管理)が役立つと思いますか?
ミカエルエリクソン

多分。または、デッドロック例外を同時実行例外に変える場合があります。
–RBarryYoung

2
INSERTステートメントのターゲットであるテーブルでTABLOCKヒントを指定すると、TABLOCKXヒントを指定した場合と同じ効果があります。(ソース:msdn.microsoft.com/en-us/library/bb510625.aspx
tuespetre

31

テーブル変数が1つの値しか保持していなければ、問題はありません。複数の行がある場合、デッドロックの新たな可能性があります。同じ会社の(1、2)および(2、1)を含むテーブル変数を使用して、2つの並行プロセス(AおよびB)が実行されるとします。

プロセスAは宛先を読み取り、行を検出せず、値「1」を挿入します。値「1」で排他的な行ロックを保持します。プロセスBは宛先を読み取り、行を検出せず、値「2」を挿入します。値「2」で排他的な行ロックを保持します。

ここで、プロセスAは行2を処理する必要があり、プロセスBは行1を処理する必要があります。他のプロセスが保持する排他ロックと互換性のないロックが必要なため、どちらのプロセスも進行できません。

複数行でのデッドロックを回避するには、行を毎回同じ順序で処理(およびテーブルにアクセス)する必要があります。質問に示されている実行プランのテーブル変数はヒープであるため、行には固有の順序がありません(これは保証されていませんが、挿入順序で読み取られる可能性が非常に高い)。

既存の計画

一貫した行処理順序の欠如は、デッドロックの機会に直接つながります。2番目の考慮事項は、キーの一意性の保証がないため、正しいハロウィーン保護を提供するにはテーブルスプールが必要であることを意味します。スプールは熱心なスプールです。つまり、すべての行がtempdbワークテーブルに書き込まれた後、挿入演算子で読み取られて再生されます。

TYPEテーブル変数のを再定義して、クラスター化されたものを含めますPRIMARY KEY

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

実行プランにクラスター化インデックスのスキャンが表示されるようになり、一意性が保証されるため、オプティマイザーはテーブルスプールを安全に削除できます。

主キー付き

MERGE128スレッドでステートメントを5000回繰り返したテストでは、クラスター化テーブル変数でデッドロックは発生しませんでした。これは観察に基づいているだけであることを強調する必要があります。クラスター化されたテーブル変数は(技術的に)さまざまな順序で行を生成することもできますが、一貫した順序の可能性は非常に大きく向上します。もちろん、新しい累積更新プログラム、サービスパック、またはSQL Serverの新しいバージョンごとに、観察された動作を再テストする必要があります。

テーブル変数の定義を変更できない場合、別の方法があります。

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

これにより、明示的なソートの導入を犠牲にして、スプールの削除(および行順序の一貫性)も実現されます。

並べ替え計画

この計画では、同じテストを使用してもデッドロックは発生しませんでした。以下の複製スクリプト:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;

8

SQL_Kiwiは非常に優れた分析を提供したと思います。データベースの問題を解決する必要がある場合は、彼の提案に従う必要があります。もちろん、アップグレード、サービスパックの適用、またはインデックスまたはインデックス付きビューの追加/変更を行うたびに、それが引き続き機能することを再テストする必要があります。

他にも3つの選択肢があります。

  1. 挿入が衝突しないようにシリアル化できます。MERGEを実行する前に、トランザクションの開始時にsp_getapplockを呼び出し、排他ロックを取得できます。もちろん、ストレステストを行う必要があります。

  2. 1つのスレッドですべての挿入を処理して、アプリサーバーが同時実行を処理できるようにすることができます。

  3. デッドロック後に自動的に再試行できます-同時実行性が高い場合、これは最も遅いアプローチです。

どちらの方法でも、ソリューションがパフォーマンスに与える影響を判断できるのは自分だけです。

通常、システムにデッドロックはありませんが、デッドロックが発生する可能性はたくさんあります。2011年には、1つの展開でミスを犯し、数時間以内に半ダースのデッドロックが発生しましたが、すべて同じシナリオに従いました。私はすぐにそれを修正し、それがその年のすべての行き詰まりでした。

システムでは主にアプローチ1を使用しています。それは私たちにとって本当にうまくいきます。


-1

考えられるもう1つのアプローチ-Mergeはロックとパフォーマンスの問題を時々示すことがわかっています-Option(MaxDop x)クエリオプションを試してみる価値があります

薄暗い遠い過去に、SQL ServerにはInsert Row Level Lockingオプションがありましたが、これは死亡したようですが、IDを持つクラスター化されたPKは挿入をクリーンにする必要があります。

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