web-dev-qa-db-ja.com

自分自身をデッドロックするMergeステートメント

次の手順があります(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つの異なるスレッドが制約を検証しているときに行(またはページ)を異なる順序でロックしているため、デッドロックしていると思います。

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

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

Query Plan Image (新しいタブで画像を表示すると、判読可能です。サイズが小さいため申し訳ありません。)

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

さて、数回にわたってすべてを調べた後、あなたの基本的な仮定は正しかったと思います。おそらくここで起こっているのは、

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

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

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

2番目のユーザーも同じページに挿入する必要がある場合は、デッドロックに陥っています。

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

可能です。代わりにTABLOCKヒントを追加するだけで、パフォーマンスに大きな影響を与えることなく問題を解決できます。

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

12
RBarryYoung

Table変数が1つの値しか保持していなくても問題はありません。複数の行があると、デッドロックが発生する可能性があります。同じ会社の(1、2)と(2、1)を含むテーブル変数を使用して実行される2つの同時プロセス(A&B)があるとします。

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

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

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

Existing plan

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

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

DROP TYPE dbo.CoUserData;

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

実行計画はクラスター化インデックスのスキャンを示し、一意性保証はオプティマイザがテーブルスプールを安全に削除できることを意味します。

With primary key

128スレッドでMERGEステートメントを5000回繰り返したテストでは、クラスター化テーブル変数でデッドロックは発生しませんでした。これは観察のみに基づいていることを強調しておきます。クラスタ化されたテーブル変数は、(technically)によって行をさまざまな順序で生成することもできますが、一貫した順序の可能性が大幅に向上します。もちろん、観察された動作は、新しい累積的な更新、サービスパック、または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);

これにより、明示的なソートを導入する代わりに、スプール(および行順の一貫性)を排除することもできます。

Sort plan

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

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;
31
Paul White 9

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

他に3つの方法があります。

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

  2. 1つのスレッドですべての挿入を処理できるため、アプリサーバーで同時実行を処理できます。

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

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

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

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

8
A-K