web-dev-qa-db-ja.com

INSERT / UPDATEストアドプロシージャ自体のデッドロック

他の特定のテーブルの行が最後に変更された日時に関する情報を含むテーブル(Database2.dbo.OrganizerDataDependencyChange)があります。これらの各テーブルには、トリガーが起動した時間でDatabase2.dbo.OrganizerDataDependencyChangeを更新するだけのストアドプロシージャ(Database2.dbo.SaveOrganizerDataDependencyChange)を呼び出すトリガーがあります。これらのトリガーは、Database1またはDatabase2のいずれかから起動されるため、データベース間の呼び出しであることがよくあります。

テーブルの2つの異なる行が更新されているとき、Database1.dbo.OrganizerDataDependencyChangeテーブルで読み取り/書き込みデッドロックが発生し、その理由がわかりません。テーブルにはインデックスが1つだけあり、クエリを完全にカバーするのはクラスター化インデックスなので、ルックアップデッドロックは発生しません。また、プロシージャには2つのステートメントがありますが、具体的には、理解するように書き直し、 IF/ELSEロジックを使用する代わりにWHERE NOT EXISTSを使用して、同時実行性の問題を回避します。そのため、このインデックスにさまざまな角度から来るべきではありません。トリガーとクロスデータベースで発生したため、問題を再現するのが難しいので、もちろんブロッキングを再現できますが、それで問題ないはずです。

誰かが私にここで何が起こっているのかを理解するのを手伝ってくれる?おそらくNOLOCKヒントまたは多分applockでそれを修正できたかもしれませんが、それがなぜ起こっているのかまだわかりません。

これがテーブルです:

CREATE TABLE [dbo].[OrganizerDataDependencyChange](
[TableID] [INT] NOT NULL,
[Database] [VARCHAR](150) NOT NULL,
[Updated] [DATETIME] NULL
)
CREATE CLUSTERED INDEX [CX_OrganizerDataDependencyChange_TableID_DB] ON [dbo].[OrganizerDataDependencyChange]
(
[TableID] ASC,
[Database] ASC
)

そして、これがストアドプロシージャです。

CREATE PROCEDURE [dbo].[SaveOrganizerDataDependencyChange]
(
@TableID int,
@Database varchar(150)
)
AS
BEGIN

SET NOCOUNT ON;

    DECLARE @rowcount INT;

INSERT INTO dbo.OrganizerDataDependencyChange
        ( TableID, [Database], Updated )
SELECT TOP 1
    TableID = @TableID,
    [Database] = ISNULL(@Database, ''),
    Updated = GETDATE()
FROM dbo.OrganizerDataDependencyChange
WHERE NOT EXISTS
(
    SELECT
        1
    FROM dbo.OrganizerDataDependencyChange
    WHERE TableID = @TableID
    AND [Database] = @Database
)

SET @rowcount = @@ROWCOUNT  --How many rows were inserted?

UPDATE dbo.OrganizerDataDependencyChange
SET Updated = GETDATE()
WHERE @rowcount = 0 --Only run this if no rows were inserted
AND TableID = @TableID
AND [Database] = ISNULL(@Database, '')

END

最後に、私がXEから取得しているいくつかのデッドロックグラフは、ばかげた入れ子になったトリガーを示しています。

デッドロック1

デッドロック2

MSSQL2014標準(違いがある場合)。分離レベルは、デフォルトのコミット読み取りです。他にも共有できることがあれば、もっと光を当ててください。

4
Zaphodb2002

私はあなたがこのパターンをよりよく見つけると思います:

BEGIN TRANSACTION;

UPDATE dbo.OrganizerDataDependencyChange WITH (HOLDLOCK)
  SET Updated = GETDATE()
  WHERE TableID = @TableID
  AND [Database] = ISNULL(@Database, '');

IF @@ROWCOUNT = 0
BEGIN
  INSERT INTO dbo.OrganizerDataDependencyChange
      ( TableID, [Database], Updated )
  VALUES(@TableID, ISNULL(@Database, ''), GETDATE());
END

COMMIT TRANSACTION;

更新する行があるかどうかを「確認」する必要があることを考えずに、その行を更新すると、2回のフルスキャンが発生する可能性があります。 それを更新してみてください。行が更新されない場合、害はなく、ファウルもありません-挿入を実行するだけでかまいません(これについては少し話します- ここ )。

6
Aaron Bertrand

以下のリンクがここで役立つと思います。最初のリンクは、問題を解決する方法を示している可能性があります。 2番目のリンクは、何が発生していて、なぜ問題が発生しているのかを説明するのに役立ちます。最後のリンクは、何が起こっているかをさらに理解するのに役立つ可能性があるいくつかの追加の調査です。

「upsert」の例を実行しています。

更新と挿入を使用した場合のロック、ブロック、および同時実行のブレークアウト

同時実行更新/挿入時にスタックします。

1
Shaulinator

アップサートを実行するときは、もう少し防御的になることを好むので、_sp_getapplock_および_sp_releaseapplock_を使用して、アップサートされたテーブルへの排他的アクセスを確保します。

以下のコード例は、最初にアップサートのターゲットとなるテーブルを作成します。

_USE tempdb;
GO
IF OBJECT_ID(N'dbo.OrganizerDataDependencyChange', N'U') IS NOT NULL
DROP TABLE dbo.OrganizerDataDependencyChange;
CREATE TABLE dbo.OrganizerDataDependencyChange
(
    PadOneRowPerPage char(5000) NOT NULL
    , TableID int NOT NULL
    , DatabaseName sysname NOT NULL
    , Updated datetime NULL
    , CONSTRAINT PK_OrganizerDataDependencyChange
        PRIMARY KEY CLUSTERED (TableID, DatabaseName)
        WITH (
            ALLOW_ROW_LOCKS = ON
            , ALLOW_PAGE_LOCKS = ON
            , PAD_INDEX = OFF
            , FILLFACTOR = 100
            )
) ON [PRIMARY]
WITH (
    DATA_COMPRESSION = NONE
    );
GO
_

複数のテーブルの更新履歴が保存されている場合、このテーブルは非常にアクティブになる可能性が高いため、char(5000)である列PadOneRowPerPageを追加しました。この列により、ページごとに1行のみが作成されることが保証されます。これにより、このテーブルでの割り当てと書き込みの競合が減少します。また、私は_DATA_COMPRESSION_がNONEに設定されていることにも注意してください。各ページを意図的に1行だけで埋めているため、データ圧縮は不要であり、実際には有害な場合があります。

実際のアップサートを実行するためのストアドプロシージャを次に示します。プロシージャへのネストを最大5レベルまで制御するために使用されるパラメータ_@CallLevel_が含まれています。これにより、プロシージャは、アプリロックを取得できなかった場合に、再試行メカニズムで自身を呼び出すことができます。

_IF OBJECT_ID(N'dbo.SaveOrganizerDataDependencyChange', N'P') IS NOT NULL
DROP PROCEDURE dbo.SaveOrganizerDataDependencyChange;
GO
CREATE PROCEDURE dbo.SaveOrganizerDataDependencyChange
(
    @TableID int
    , @DatabaseName varchar(150)
    , @CallLevel int = 1
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @msg nvarchar(1000);
    DECLARE @rowcount int;
    DECLARE @Updated TABLE (
        Updated datetime NOT NULL
    );
    DECLARE @AppLockResult int;
    DECLARE @Result int;

    IF @CallLevel > 5
    BEGIN
        SET @msg = N'Maximum nesting level reached for SaveOrganizerDataDependencyChange';
        THROW 50002, @msg, 1;
        RETURN 0;
    END

    EXEC @AppLockResult = sys.sp_getapplock 
        @Resource = N'SaveOrganizerDataDependencyChange_Resource'
        , @LockMode = 'Exclusive' /*  Shared, Update, IntentShared, 
                                      IntentExclusive, or Exclusive */
        , @LockOwner = 'Transaction'
        , @LockTimeout = NULL --use @@LOCK_TIMEOUT
        , @DbPrincipal = N'public'; /* only run if the caller is a member 
                                       of this role */

    IF @AppLockResult >= 0
    BEGIN
        UPDATE dbo.OrganizerDataDependencyChange
        SET Updated = GETDATE()
        OUTPUT 1 INTO @Updated (Updated)
        WHERE TableID = @TableID
            AND DatabaseName = @DatabaseName;

        IF NOT EXISTS (SELECT 1 FROM @Updated)
        BEGIN
            INSERT INTO dbo.OrganizerDataDependencyChange (
                TableID, DatabaseName, Updated)
            VALUES (@TableID, @DatabaseName, GETDATE());
        END

        EXEC sys.sp_releaseapplock 
            @Resource = N'SaveOrganizerDataDependencyChange_Resource'
            , @LockOwner = 'Transaction'
            , @DbPrincipal = N'public'; /* only run if the caller is a 
                                           member of this role */

        RETURN CONVERT(int, 1); --success
    END
    ELSE 
    BEGIN
        SET @CallLevel += 1;
        EXEC @Result = dbo.SaveOrganizerDataDependencyChange 
                          @TableID
                          , @DatabaseName
                          , @CallLevel;
        IF @Result = 0 
        BEGIN
            SET @msg = N'SaveOrganizerDataDependencyChange Could not obtain app lock.';
            THROW 50001, @msg, 1;
            RETURN 0;
        END
    END
END
GO
_

アーロンが彼の回答で使用したように、更新が成功したかどうかを判断するために私が_@@ROWCOUNT_を使用していないことに気付くでしょう。 SQL Serverがストアドプロシージャを実行する方が少し速いので、_@@ROWCOUNT_を使用することを決定できます。ただし、将来、更新コードと挿入コードの間に_@@ROWCOUNT_値に影響を与える可能性のあるコードを追加すると決定した場合にコードがより堅牢になるため、テーブル変数の使用を選択しました。

ここでは、上で作成したアップサートプロシージャを呼び出すトリガーを持つテーブルを作成しています。

_IF OBJECT_ID(N'dbo.SomeTable', N'U') IS NOT NULL
DROP TABLE dbo.SomeTable;
CREATE TABLE dbo.SomeTable
(
    ID int NOT NULL IDENTITY(1,1)
        CONSTRAINT PK_SomeTable
        PRIMARY KEY CLUSTERED
) ON [PRIMARY] 
WITH (DATA_COMPRESSION = PAGE);
GO

CREATE TRIGGER dbo.upd_SomeTable
ON dbo.SomeTable
AFTER UPDATE, INSERT
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @Ret int;
    DECLARE @TableID int = OBJECT_ID(N'dbo.SomeTable', N'U');
    DECLARE @DatabaseName sysname = DB_NAME();
    BEGIN TRY
        BEGIN TRANSACTION
        EXEC @Ret = dbo.SaveOrganizerDataDependencyChange 
            @TableID = @TableID
            , @DatabaseName = @DatabaseName;
        IF @Ret = 1
        BEGIN
            COMMIT TRANSACTION;
        END
        ELSE
        BEGIN
            ROLLBACK TRANSACTION;
        END
    END TRY
    BEGIN CATCH
        DECLARE @msg nvarchar(2048);
        DECLARE @err int;
        DECLARE @state int;
        SET @msg = 50000 + ERROR_MESSAGE();
        SET @err = ERROR_NUMBER();
        SET @state = ERROR_STATE();
        IF @@TRANCOUNT > 0 
        BEGIN
            ROLLBACK TRANSACTION;
        END
        ;THROW @err, @msg, @state;
    END CATCH
END
GO
_

ここでは、このコードをできるだけ多くのクエリウィンドウで実行して、アップサートプロシージャのデッドロックをテストし、パフォーマンスの感覚を得ています。 _GO 10000_はINSERTステートメントを10,000回実行し、テーブルでの非常に重いアクティビティをシミュレートします。このステートメントの同時実行が多いほど良いです。このコードを少なくとも5セット同時に実行し、デッドロックやエラーが発生せず、開発マシンで8秒以内に50,000回の実行が完了しました。

_SET NOCOUNT ON;
GO
INSERT INTO dbo.SomeTable DEFAULT VALUES;
GO 10000 --run the above insert 10,000 times
_

アップサートされたテーブルの内容を表示します。

_SELECT *
FROM dbo.OrganizerDataDependencyChange;
_
+ ------------ + -------------- + --------------------- ---- + 
 | TableID |データベース名|更新された| 
 + ------------ + -------------- + -------------- ----------- + 
 | 1861581670 | tempdb | 2017-04-04 08:07:59.497 | 
 + ------------ + -------------- + ----- -------------------- +
1
Max Vernon

すべてのプロセスのように、このパターンをしばらく維持する場合は、総所有コストに関するアーロンのアップサートの例の提案が気に入っています。

しかし、単純な変更が存在しない可能性があることもわかります。pkがnullである左外部結合に変更します。バッチ処理でダーティリードが問題にならない場合は、これらのリードにAdd With(NoLock)を追加します。

0
Rob Presto