他の特定のテーブルの行が最後に変更された日時に関する情報を含むテーブル(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から取得しているいくつかのデッドロックグラフは、ばかげた入れ子になったトリガーを示しています。
MSSQL2014標準(違いがある場合)。分離レベルは、デフォルトのコミット読み取りです。他にも共有できることがあれば、もっと光を当ててください。
私はあなたがこのパターンをよりよく見つけると思います:
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回のフルスキャンが発生する可能性があります。 それを更新してみてください。行が更新されない場合、害はなく、ファウルもありません-挿入を実行するだけでかまいません(これについては少し話します- ここ )。
以下のリンクがここで役立つと思います。最初のリンクは、問題を解決する方法を示している可能性があります。 2番目のリンクは、何が発生していて、なぜ問題が発生しているのかを説明するのに役立ちます。最後のリンクは、何が起こっているかをさらに理解するのに役立つ可能性があるいくつかの追加の調査です。
アップサートを実行するときは、もう少し防御的になることを好むので、_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 | + ------------ + -------------- + ----- -------------------- +
すべてのプロセスのように、このパターンをしばらく維持する場合は、総所有コストに関するアーロンのアップサートの例の提案が気に入っています。
しかし、単純な変更が存在しない可能性があることもわかります。pkがnullである左外部結合に変更します。バッチ処理でダーティリードが問題にならない場合は、これらのリードにAdd With(NoLock)を追加します。