Il n'y aurait pas de problème si la variable de table ne contenait qu'une seule valeur. Avec plusieurs lignes, il existe une nouvelle possibilité de blocage. Supposons que deux processus simultanés (A et B) s'exécutent avec des variables de table contenant (1, 2) et (2, 1) pour la même société.
Le processus A lit la destination, ne trouve aucune ligne et insère la valeur «1». Il détient un verrou de ligne exclusif sur la valeur «1». Le processus B lit la destination, ne trouve aucune ligne et insère la valeur «2». Il détient un verrou de ligne exclusif sur la valeur «2».
Le processus A doit désormais traiter la ligne 2 et le processus B doit traiter la ligne 1. Aucun des deux processus ne peut progresser car il nécessite un verrou incompatible avec le verrou exclusif détenu par l'autre processus.
Pour éviter les blocages avec plusieurs lignes, les lignes doivent être traitées (et les tables accédées) dans le même ordre à chaque fois . La variable de table dans le plan d'exécution montré dans la question est un tas, donc les lignes n'ont pas d'ordre intrinsèque (elles sont très susceptibles d'être lues dans l'ordre d'insertion, bien que cela ne soit pas garanti):
L'absence d'un ordre de traitement des lignes cohérent conduit directement à l'opportunité de blocage. Une deuxième considération est que l'absence d'une garantie d'unicité clé signifie qu'une bobine de table est nécessaire pour fournir une protection Halloween correcte. Le spool est un spool enthousiaste, ce qui signifie que toutes les lignes sont écrites dans une table de travail tempdb avant d'être lues et relues pour l'opérateur d'insertion.
Redéfinir la TYPE
variable de table pour inclure un cluster PRIMARY KEY
:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
Le plan d'exécution affiche désormais une analyse de l'index clusterisé et la garantie d'unicité signifie que l'optimiseur est en mesure de supprimer le spouleur de table en toute sécurité:
Dans les tests avec 5000 itérations de l' MERGE
instruction sur 128 threads, aucun blocage n'a eu lieu avec la variable de table en cluster. Je dois souligner que ce n'est que sur la base de l'observation; la variable de table en cluster pourrait également ( techniquement ) produire ses lignes dans une variété d'ordres, mais les chances d'un ordre cohérent sont très considérablement améliorées. Le comportement observé devrait être retesté pour chaque nouvelle mise à jour cumulative, service pack ou nouvelle version de SQL Server, bien sûr.
Si la définition de la variable de table ne peut pas être modifiée, il existe une autre alternative:
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);
Cela permet également d'éliminer le spool (et la cohérence de l'ordre des lignes) au prix de l'introduction d'un tri explicite:
Ce plan n'a également produit aucun blocage en utilisant le même test. Script de reproduction ci-dessous:
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;