La MERGE
declaración tiene una sintaxis compleja y una implementación aún más compleja, pero esencialmente la idea es unir dos tablas, filtrar a filas que deben cambiarse (insertarse, actualizarse o eliminarse) y luego realizar los cambios solicitados. Dados los siguientes datos de muestra:
DECLARE @CategoryItem AS TABLE
(
CategoryId integer NOT NULL,
ItemId integer NOT NULL,
PRIMARY KEY (CategoryId, ItemId),
UNIQUE (ItemId, CategoryId)
);
DECLARE @DataSource AS TABLE
(
CategoryId integer NOT NULL,
ItemId integer NOT NULL
PRIMARY KEY (CategoryId, ItemId)
);
INSERT @CategoryItem
(CategoryId, ItemId)
VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 1),
(2, 3),
(3, 5),
(3, 6),
(4, 5);
INSERT @DataSource
(CategoryId, ItemId)
VALUES
(2, 2);
Objetivo
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 2 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 3 ║
║ 3 ║ 5 ║
║ 4 ║ 5 ║
║ 3 ║ 6 ║
╚════════════╩════════╝
Fuente
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
El resultado deseado es reemplazar los datos en el destino con datos de la fuente, pero solo para CategoryId = 2
. Siguiendo la descripción de lo MERGE
anterior, deberíamos escribir una consulta que combine el origen y el destino solo en las claves, y filtre las filas solo en las WHEN
cláusulas:
MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE
AND TARGET.CategoryId = 2
THEN DELETE
WHEN NOT MATCHED BY TARGET
AND SOURCE.CategoryId = 2
THEN INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
Esto da los siguientes resultados:
╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE ║ 2 ║ 1 ║
║ INSERT ║ 2 ║ 2 ║
║ DELETE ║ 2 ║ 3 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 2 ║
║ 3 ║ 5 ║
║ 3 ║ 6 ║
║ 4 ║ 5 ║
╚════════════╩════════╝
El plan de ejecución es:
Observe que ambas tablas se escanean completamente. Podríamos pensar que esto es ineficiente, porque solo las filas CategoryId = 2
se verán afectadas en la tabla de destino. Aquí es donde entran las advertencias en Books Online. Un intento equivocado de optimizar para tocar solo las filas necesarias en el objetivo es:
MERGE INTO @CategoryItem AS TARGET
USING
(
SELECT CategoryId, ItemId
FROM @DataSource AS ds
WHERE CategoryId = 2
) AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
La lógica en la ON
cláusula se aplica como parte de la unión. En este caso, la combinación es una combinación externa completa (consulte esta entrada de Libros en línea para saber por qué). Al aplicar la verificación para la categoría 2 en las filas de destino como parte de una unión externa, en última instancia, se eliminan las filas con un valor diferente (porque no coinciden con la fuente):
╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE ║ 1 ║ 1 ║
║ DELETE ║ 1 ║ 2 ║
║ DELETE ║ 1 ║ 3 ║
║ DELETE ║ 2 ║ 1 ║
║ INSERT ║ 2 ║ 2 ║
║ DELETE ║ 2 ║ 3 ║
║ DELETE ║ 3 ║ 5 ║
║ DELETE ║ 3 ║ 6 ║
║ DELETE ║ 4 ║ 5 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
La causa raíz es la misma razón por la que los predicados se comportan de manera diferente en una ON
cláusula de unión externa que si se especifica en la WHERE
cláusula. La MERGE
sintaxis (y la implementación de la unión según las cláusulas especificadas) solo hacen que sea más difícil ver que esto es así.
La guía en Books Online (ampliada en la entrada Optimizing Performance ) ofrece una guía que garantizará que la semántica correcta se exprese utilizando la MERGE
sintaxis, sin que el usuario tenga que comprender necesariamente todos los detalles de implementación, o explicar las formas en que el optimizador podría reorganizarse legítimamente cosas por razones de eficiencia de ejecución.
La documentación ofrece tres formas potenciales de implementar el filtrado temprano:
La especificación de una condición de filtrado en la WHEN
cláusula garantiza resultados correctos, pero puede significar que se leen y procesan más filas de las tablas de origen y destino de las estrictamente necesarias (como se ve en el primer ejemplo).
La actualización a través de una vista que contiene la condición de filtrado también garantiza resultados correctos (dado que las filas modificadas deben ser accesibles para la actualización a través de la vista) pero esto requiere una vista dedicada y una que siga las condiciones extrañas para actualizar las vistas.
El uso de una expresión de tabla común conlleva riesgos similares al agregar predicados a la ON
cláusula, pero por razones ligeramente diferentes. En muchos casos será seguro, pero requiere un análisis experto del plan de ejecución para confirmar esto (y pruebas prácticas exhaustivas). Por ejemplo:
WITH TARGET AS
(
SELECT *
FROM @CategoryItem
WHERE CategoryId = 2
)
MERGE INTO TARGET
USING
(
SELECT CategoryId, ItemId
FROM @DataSource
WHERE CategoryId = 2
) AS SOURCE ON
SOURCE.ItemId = TARGET.ItemId
AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
INSERT (CategoryId, ItemId)
VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
DELETE
OUTPUT
$ACTION,
ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;
Esto produce resultados correctos (no repetidos) con un plan más óptimo:
El plan solo lee filas para la categoría 2 de la tabla de destino. Esto podría ser una consideración importante de rendimiento si la tabla de destino es grande, pero es demasiado fácil equivocarse con la MERGE
sintaxis.
A veces, es más fácil escribir MERGE
como operaciones DML separadas. Este enfoque incluso puede funcionar mejor que uno solo MERGE
, un hecho que a menudo sorprende a las personas.
DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS
(
SELECT 1
FROM @DataSource AS ds
WHERE
ds.ItemId = ci.ItemId
AND ds.CategoryId = ci.CategoryId
);
INSERT @CategoryItem
SELECT
ds.CategoryId,
ds.ItemId
FROM @DataSource AS ds
WHERE
ds.CategoryId = 2;