FUSIONNER un sous-ensemble de la table cible

71

J'essaie d'utiliser une MERGEinstruction pour insérer ou supprimer des lignes d'une table, mais je souhaite uniquement agir sur un sous-ensemble de ces lignes. La documentation pour MERGEa un avertissement assez fort libellé:

Il est important de spécifier uniquement les colonnes de la table cible utilisées à des fins de correspondance. Autrement dit, spécifiez les colonnes de la table cible qui sont comparées à la colonne correspondante de la table source. N'essayez pas d'améliorer les performances des requêtes en filtrant les lignes de la table cible dans la clause ON, par exemple en spécifiant AND NOT target_table.column_x = value. Cela pourrait retourner des résultats inattendus et incorrects.

mais c'est exactement ce qu'il me semble devoir faire pour faire mon MERGEtravail.

Les données que j'ai sont une table standard de jointure plusieurs-à-plusieurs d'éléments (par exemple, quels éléments sont inclus dans quelles catégories) comme ceci:

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

Ce que je dois faire est de remplacer efficacement toutes les lignes d'une catégorie spécifique par une nouvelle liste d'éléments. Ma tentative initiale de faire ceci ressemble à ceci:

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 ;

Cela semble fonctionner dans mes tests, mais je fais exactement ce que MSDN m'avertit explicitement de ne pas le faire. Cela me fait craindre des problèmes inattendus plus tard, mais je ne vois pas d'autre moyen de MERGEne modifier que les lignes affectées par la valeur de champ spécifique ( CategoryId = 2) et d'ignorer les lignes d'autres catégories.

Existe-t-il une méthode "plus correcte" pour obtenir le même résultat? Et quels sont les "résultats inattendus ou incorrects" sur lesquels MSDN m'avertit?

KutuluMike
la source
Oui, la documentation serait plus utile si elle contenait un exemple concret de "résultats inattendus et incorrects".
AK
3
@AlexKuznetsov Il y a un exemple ici .
Paul White
@SQLKiwi merci pour le lien - IMO la documentation serait bien meilleure si elle était référée depuis la page d'origine.
AK
1
@ AlexKuznetsov D'accord. Malheureusement, la réorganisation de BOL pour 2012 l'a brisé, entre autres. Il était très bien lié dans la documentation R2 2008.
Paul White

Réponses:

103

L' MERGEinstruction a une syntaxe complexe et une implémentation encore plus complexe, mais il s'agit essentiellement de joindre deux tables, de filtrer les lignes à modifier (insérées, mises à jour ou supprimer), puis d'effectuer les modifications demandées. À partir des exemples de données suivants:

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);

Cible

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          1       1 
          2       1 
          1       2 
          1       3 
          2       3 
          3       5 
          4       5 
          3       6 
╚════════════╩════════╝

La source

╔════════════╦════════╗
 CategoryId  ItemId 
╠════════════╬════════╣
          2       2 
╚════════════╩════════╝

Le résultat souhaité est de remplacer les données de la cible par des données de la source, mais uniquement pour CategoryId = 2. Après la description MERGEdonnée ci-dessus, nous devrions écrire une requête qui joint la source et la cible sur les clés uniquement, et filtrer les lignes uniquement dans les WHENclauses:

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
;

Cela donne les résultats suivants:

╔═════════╦════════════╦════════╗
 $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 
╚════════════╩════════╝

Le plan d'exécution est le suivant: Fusionner le plan

Notez que les deux tables sont entièrement numérisées. Nous pourrions penser que cela est inefficace, car seules les lignes où CategoryId = 2seront affectées la table cible. C’est ici que les avertissements de la documentation en ligne entrent en jeu. Une tentative erronée d’optimisation pour ne toucher que les lignes nécessaires de la cible est la suivante:

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 logique de la ONclause est appliquée dans le cadre de la jointure. Dans ce cas, la jointure est une jointure externe complète (voir pourquoi cette entrée dans la documentation en ligne ). L'application de la vérification de la catégorie 2 sur les lignes cibles dans le cadre d'une jointure externe entraîne la suppression des lignes de valeur différente (car elles ne correspondent pas à la source):

╔═════════╦════════════╦════════╗
 $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 cause fondamentale est la même raison pour laquelle les prédicats se comportent différemment dans une ONclause de jointure externe que s'ils le spécifient dans la WHEREclause. La MERGEsyntaxe (et l'implémentation de la jointure en fonction des clauses spécifiées) rendent simplement plus difficile de voir qu'il en est ainsi.

Les instructions de la documentation en ligne (développées dans l' entrée Optimizing Performance ) fournissent des indications permettant de s'assurer que la sémantique correcte est exprimée à l'aide de la MERGEsyntaxe, sans que l'utilisateur ait nécessairement à comprendre tous les détails de la mise en œuvre ni à rendre compte de la manière dont l'optimiseur pourrait légitimement réorganiser. les choses pour des raisons d'efficacité d'exécution.

La documentation offre trois méthodes possibles pour implémenter le filtrage précoce:

La spécification d'une condition de filtrage dans la WHENclause garantit des résultats corrects, mais peut signifier que davantage de lignes lues et traitées à partir des tables source et cible sont strictement nécessaires (comme le montre le premier exemple).

La mise à jour via une vue contenant la condition de filtrage garantit également des résultats corrects (puisque les lignes modifiées doivent être accessibles pour la mise à jour via la vue), mais cela nécessite une vue dédiée et une condition qui suit les conditions impaires pour la mise à jour des vues.

L'utilisation d'une expression de table commune comporte des risques similaires à l'ajout de prédicats à la ONclause, mais pour des raisons légèrement différentes. Dans de nombreux cas, cela sera sûr, mais cela nécessite une analyse experte du plan d'exécution (ainsi que des tests pratiques approfondis). Par exemple:

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
;

Cela produit des résultats corrects (non répétés) avec un plan plus optimal:

Fusionner le plan 2

Le plan ne lit que les lignes de la catégorie 2 à partir de la table cible. Cela peut constituer un facteur de performance important si la table cible est grande, mais il est trop facile de se tromper en utilisant la MERGEsyntaxe.

Parfois, il est plus facile d’écrire les MERGEopérations DML distinctes. Cette approche peut même donner de meilleurs résultats qu’une seule MERGE, ce qui surprend souvent les gens.

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
la source
Je sais que la question est très ancienne ... mais vous pouvez préciser "L'utilisation d'une expression de table commune comporte des risques similaires à l'ajout de prédicats à la clause ON, mais pour des raisons légèrement différentes." Je sais que BOL a également un avertissement vague similaire "Cette méthode est similaire à la spécification de critères de recherche supplémentaires dans la clause ON et peut produire des résultats incorrects. Nous vous recommandons d’éviter d’utiliser cette méthode ...". La méthode CTE semble résoudre mon cas d'utilisation, mais je me demande s'il existe un scénario que je n'envisage pas.
Henry Lee