如果表变量仅保留一个值,则不会有问题。对于多行,存在死锁的新可能性。假设两个并发进程(A&B)的表变量包含同一公司的(1、2)和(2、1)。
进程A读取目标,未找到任何行,并插入值“ 1”。它在值“ 1”上具有排他的行锁。进程B读取目标,找不到行,并插入值'2'。它在值'2'上拥有排他行锁。
现在,进程A需要处理第2行,而进程B需要处理第1行。这两个进程都无法取得进展,因为它需要的锁与另一个进程所持有的排他锁不兼容。
为了避免多行出现死锁,每次都需要以相同顺序处理(和访问表)行。问题中显示的执行计划中的表变量是堆,因此行没有内在顺序(尽管不保证一定能以插入顺序读取它们):
缺少一致的行处理顺序将直接导致死锁机会。第二个考虑因素是,缺少密钥唯一性保证意味着必须提供表后台处理程序才能提供正确的万圣节保护。假脱机程序是一个急切的假脱机程序,这意味着所有行都将被写入tempdb工作表,然后再读取并重播给Insert运算符。
重新定义TYPE
table变量以包含一个集群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次迭代的测试中,集群表变量未发生死锁。我应该强调,这仅仅是基于观察;聚簇表变量也可以(技术上)以各种顺序产生其行,但是一致顺序的机会大大增加了。当然,对于每个新的累积更新,Service Pack或新版本的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;