UNIR un subconjunto de la tabla de destino

71

Estoy tratando de usar una MERGEdeclaración para insertar o eliminar filas de una tabla, pero solo quiero actuar en un subconjunto de esas filas. La documentación para MERGEtiene una advertencia bastante redactada:

Es importante especificar solo las columnas de la tabla de destino que se utilizan con fines de coincidencia. Es decir, especifique columnas de la tabla de destino que se comparan con la columna correspondiente de la tabla de origen. No intente mejorar el rendimiento de la consulta filtrando las filas de la tabla de destino en la cláusula ON, como especificando AND NOT target_table.column_x = value. Hacerlo puede devolver resultados inesperados e incorrectos.

pero esto es exactamente lo que parece que tengo que hacer para hacer mi MERGEtrabajo.

Los datos que tengo son una tabla estándar de unión de muchos a muchos a categorías (por ejemplo, qué elementos se incluyen en qué categorías) de esta manera:

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

Lo que necesito hacer es reemplazar efectivamente todas las filas en una categoría específica con una nueva lista de elementos. Mi intento inicial de hacer esto se ve así:

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

Esto parece estar funcionando en mis pruebas, pero estoy haciendo exactamente lo que MSDN me advierte explícitamente que no haga. Esto me preocupa de tener problemas inesperados más adelante, pero no puedo ver ninguna otra manera de hacer que mis MERGEúnicas filas afecten con el valor de campo específico ( CategoryId = 2) e ignore las filas de otras categorías.

¿Existe una forma "más correcta" de lograr este mismo resultado? ¿Y cuáles son los "resultados inesperados o incorrectos" que MSDN me advierte?

KutuluMike
fuente
Sí, la documentación sería más útil si tuviera un ejemplo concreto de "resultados inesperados e incorrectos".
AK
3
@AlexKuznetsov Hay un ejemplo aquí .
Paul White
@SQLKiwi gracias por el enlace: en mi opinión, la documentación sería mucho mejor si se derivara de la página original.
AK
1
@AlexKuznetsov De acuerdo. Desafortunadamente, la reorganización de BOL para 2012 rompió eso, entre muchas otras cosas. Se vinculó bastante bien en la documentación de 2008 R2.
Paul White

Respuestas:

103

La MERGEdeclaració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 MERGEanterior, deberíamos escribir una consulta que combine el origen y el destino solo en las claves, y filtre las filas solo en las WHENclá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: Plan de fusión

Observe que ambas tablas se escanean completamente. Podríamos pensar que esto es ineficiente, porque solo las filas CategoryId = 2se 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 ONclá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 ONcláusula de unión externa que si se especifica en la WHEREcláusula. La MERGEsintaxis (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 MERGEsintaxis, 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 WHENclá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 ONclá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:

Plan de fusión 2

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 MERGEsintaxis.

A veces, es más fácil escribir MERGEcomo 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;
Paul White
fuente
Sé que esta es una pregunta muy antigua ... pero cualquier posibilidad de que pueda explicar "Usar una expresión de tabla común conlleva riesgos similares para agregar predicados a la cláusula ON, pero por razones ligeramente diferentes". Sé que BOL también tiene una advertencia igualmente vaga: "Este método es similar a especificar criterios de búsqueda adicionales en la cláusula ON y puede producir resultados incorrectos. Recomendamos que evite usar este método ...". El método CTE parece resolver mi caso de uso, sin embargo, me pregunto si hay un escenario que no estoy considerando.
Henry Lee