Não haveria problema se a variável da tabela tivesse apenas um valor. Com várias linhas, há uma nova possibilidade de conflito. Suponha que dois processos simultâneos (A e B) sejam executados com variáveis de tabela contendo (1, 2) e (2, 1) para a mesma empresa.
O processo A lê o destino, não encontra linha e insere o valor '1'. Ele contém um bloqueio de linha exclusivo no valor '1'. O processo B lê o destino, não encontra linha e insere o valor '2'. Ele contém um bloqueio de linha exclusivo no valor '2'.
Agora, o processo A precisa processar a linha 2 e o processo B precisa processar a linha 1. Nenhum processo pode progredir porque requer um bloqueio que é incompatível com o bloqueio exclusivo mantido pelo outro processo.
Para evitar conflitos com várias linhas, as linhas precisam ser processadas (e as tabelas acessadas) na mesma ordem todas as vezes . A variável de tabela no plano de execução mostrada na pergunta é uma pilha, portanto as linhas não têm ordem intrínseca (é bem provável que sejam lidas em ordem de inserção, embora isso não seja garantido):
A falta de ordem consistente de processamento de linha leva diretamente à oportunidade de conflito. Uma segunda consideração é que a falta de uma garantia de exclusividade importante significa que um spool de tabela é necessário para fornecer a proteção correta de Halloween. O spool é um spool ansioso, o que significa que todas as linhas são gravadas em uma mesa de trabalho tempdb antes de serem lidas novamente e reproduzidas para o operador Insert.
Redefinindo a TYPE
variável da tabela para incluir um cluster PRIMARY KEY
:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
O plano de execução agora mostra uma varredura do índice em cluster e a garantia de exclusividade significa que o otimizador pode remover com segurança o spool de tabela:
Nos testes com 5000 iterações da MERGE
instrução em 128 threads, nenhum conflito ocorreu com a variável de tabela em cluster. Devo enfatizar que isso é apenas com base na observação; a variável de tabela em cluster também pode ( tecnicamente ) produzir suas linhas em uma variedade de pedidos, mas as chances de um pedido consistente são bastante aumentadas. O comportamento observado precisaria ser testado novamente para cada nova atualização cumulativa, service pack ou nova versão do SQL Server, é claro.
Caso a definição da variável da tabela não possa ser alterada, existe outra alternativa:
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);
Isso também alcança a eliminação do spool (e a consistência da ordem das linhas) ao custo da introdução de uma classificação explícita:
Esse plano também não produziu conflitos usando o mesmo teste. Roteiro de reprodução abaixo:
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;