No habría problema si la variable de tabla solo tuviera un valor. Con varias filas, hay una nueva posibilidad de punto muerto. Suponga que dos procesos concurrentes (A y B) se ejecutan con variables de tabla que contienen (1, 2) y (2, 1) para la misma compañía.
El proceso A lee el destino, no encuentra fila e inserta el valor '1'. Tiene un bloqueo de fila exclusivo en el valor '1'. El proceso B lee el destino, no encuentra fila e inserta el valor '2'. Tiene un bloqueo de fila exclusivo en el valor '2'.
Ahora el proceso A necesita procesar la fila 2, y el proceso B necesita procesar la fila 1. Ninguno de los procesos puede avanzar porque requiere un bloqueo que sea incompatible con el bloqueo exclusivo del otro proceso.
Para evitar puntos muertos con varias filas, las filas deben procesarse (y acceder a las tablas) en el mismo orden cada vez . La variable de tabla en el plan de ejecución que se muestra en la pregunta es un montón, por lo que las filas no tienen un orden intrínseco (es muy probable que se lean en orden de inserción, aunque esto no está garantizado):
La falta de un orden de procesamiento de fila consistente conduce directamente a la oportunidad de punto muerto. Una segunda consideración es que la falta de una garantía de singularidad clave significa que se necesita un carrete de mesa para proporcionar la protección correcta de Halloween. El spool es un spool ansioso, lo que significa que todas las filas se escriben en una tabla de trabajo tempdb antes de volver a leerse y reproducirse para el operador Insertar.
Redefiniendo la TYPE
variable de la tabla para incluir un clúster PRIMARY KEY
:
DROP TYPE dbo.CoUserData;
CREATE TYPE dbo.CoUserData
AS TABLE
(
MyKey integer NOT NULL PRIMARY KEY CLUSTERED,
MyValue integer NOT NULL
);
El plan de ejecución ahora muestra un escaneo del índice agrupado y la garantía de unicidad significa que el optimizador puede eliminar de forma segura el Table Spool:
En las pruebas con 5000 iteraciones de la MERGE
declaración en 128 subprocesos, no se produjeron puntos muertos con la variable de tabla agrupada. Debo enfatizar que esto es solo en base a la observación; la variable de tabla en clúster también podría ( técnicamente ) producir sus filas en una variedad de órdenes, pero las posibilidades de un orden consistente son mucho mayores. El comportamiento observado tendría que volver a probarse para cada nueva actualización acumulativa, service pack o nueva versión de SQL Server, por supuesto.
En caso de que la definición de la variable de la tabla no se pueda cambiar, hay otra 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);
Esto también logra la eliminación del carrete (y la consistencia del orden de las filas) a costa de introducir un tipo explícito:
Este plan tampoco produjo puntos muertos utilizando la misma prueba. Guión de reproducción a continuación:
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;