テーブル変数が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
);
実行プランにクラスター化インデックスのスキャンが表示されるようになり、一意性が保証されるため、オプティマイザーはテーブルスプールを安全に削除できます。
MERGE
128スレッドでステートメントを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;