L' MERGE
instruction 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 MERGE
donné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 WHEN
clauses:
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:
Notez que les deux tables sont entièrement numérisées. Nous pourrions penser que cela est inefficace, car seules les lignes où CategoryId = 2
seront 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 ON
clause 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 ON
clause de jointure externe que s'ils le spécifient dans la WHERE
clause. La MERGE
syntaxe (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 MERGE
syntaxe, 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 WHEN
clause 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 ON
clause, 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:
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 MERGE
syntaxe.
Parfois, il est plus facile d’écrire les MERGE
opé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;