Не было бы проблемы, если бы табличная переменная содержала только одно значение. С несколькими рядами появилась новая возможность тупика. Предположим, что два параллельных процесса (A & B) выполняются с табличными переменными, содержащими (1, 2) и (2, 1) для одной и той же компании.
Процесс A считывает место назначения, не находит строки и вставляет значение «1». Он содержит исключительную блокировку строки со значением '1'. Процесс B читает пункт назначения, не находит строки и вставляет значение '2'. Он содержит эксклюзивную блокировку строки со значением '2'.
Теперь процессу A требуется обработать строку 2, а процессу B - обработать строку 1. Ни один из процессов не может добиться прогресса, поскольку ему требуется блокировка, несовместимая с исключительной блокировкой, удерживаемой другим процессом.
Чтобы избежать взаимных блокировок с несколькими строками, необходимо каждый раз обрабатывать строки (и обращаться к таблицам) в одном и том же порядке . Переменная таблицы в плане выполнения, показанная в вопросе, представляет собой кучу, поэтому строки не имеют внутреннего порядка (они, скорее всего, будут прочитаны в порядке вставки, хотя это не гарантируется):
Отсутствие согласованного порядка обработки строк приводит к возможности тупиковой ситуации. Второе соображение заключается в том, что отсутствие ключевой гарантии уникальности означает, что для обеспечения правильной защиты к Хэллоуину необходим Table Spool. Спул - это нетерпеливая шпуля, то есть все строки записываются на рабочий стол tempdb, а затем читаются и воспроизводятся для оператора Insert.
Переопределение TYPE
табличной переменной для включения в кластер PRIMARY KEY
:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
План выполнения теперь показывает сканирование кластеризованного индекса, а гарантия уникальности означает, что оптимизатор может безопасно удалить таблицу Spool:
В тестах с 5000 итерациями MERGE
оператора в 128 потоках не возникало взаимных блокировок с переменной кластеризованной таблицы. Я должен подчеркнуть, что это только на основе наблюдения; переменная кластеризованной таблицы также ( технически ) может производить свои строки в различных порядках, но шансы на согласованный порядок значительно повышаются. Разумеется, наблюдаемое поведение необходимо будет повторно тестировать для каждого нового накопительного обновления, пакета обновления или новой версии 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;