Die MERGE
Anweisung hat eine komplexe Syntax und eine noch komplexere Implementierung. Grundsätzlich besteht die Idee jedoch darin, zwei Tabellen zu verknüpfen, nach Zeilen zu filtern, die geändert (eingefügt, aktualisiert oder gelöscht) werden müssen, und dann die angeforderten Änderungen vorzunehmen. Angesichts der folgenden Beispieldaten:
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);
Ziel
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 1 ║ 1 ║
║ 2 ║ 1 ║
║ 1 ║ 2 ║
║ 1 ║ 3 ║
║ 2 ║ 3 ║
║ 3 ║ 5 ║
║ 4 ║ 5 ║
║ 3 ║ 6 ║
╚════════════╩════════╝
Quelle
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║ 2 ║ 2 ║
╚════════════╩════════╝
Das gewünschte Ergebnis besteht darin, Daten im Ziel durch Daten aus der Quelle zu ersetzen, jedoch nur für CategoryId = 2
. Nach der MERGE
obigen Beschreibung sollten wir eine Abfrage schreiben, die die Quelle und das Ziel nur mit den Schlüsseln verknüpft und die Zeilen nur in den WHEN
Klauseln filtert :
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
;
Daraus ergeben sich folgende Ergebnisse:
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
Der Ausführungsplan lautet:
Beachten Sie, dass beide Tabellen vollständig gescannt werden. Wir könnten dies für ineffizient halten, da nur Zeilen in der CategoryId = 2
Zieltabelle betroffen sind. Hier kommen die Warnungen in der Onlinedokumentation ins Spiel. Ein fehlgeschlagener Versuch zur Optimierung, um nur die erforderlichen Zeilen im Ziel zu berühren, ist:
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
;
Die Logik in der ON
Klausel wird als Teil des Joins angewendet. In diesem Fall kommen die eine vollständige äußere Verknüpfung (siehe diesen Online Eintrag warum). Das Anwenden der Prüfung für Kategorie 2 auf die Zielzeilen als Teil eines Outer Joins führt letztendlich dazu, dass Zeilen mit einem anderen Wert gelöscht werden (da sie nicht mit der Quelle übereinstimmen):
╔═════════╦════════════╦════════╗
║ $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 ║
╚════════════╩════════╝
Die Hauptursache ist derselbe Grund, warum Prädikate sich in einer Outer Join- ON
Klausel anders verhalten als in der WHERE
Klausel angegeben. Die MERGE
Syntax (und die Join-Implementierung in Abhängigkeit von den angegebenen Klauseln) erschweren es nur, dies zu erkennen.
Die Anleitung in der Onlinedokumentation (erweitert im Eintrag " Optimizing Performance" ) bietet Anleitungen, die sicherstellen, dass die korrekte Semantik mithilfe der MERGE
Syntax ausgedrückt wird , ohne dass der Benutzer alle Implementierungsdetails verstehen oder die Art und Weise berücksichtigen muss, in der das Optimierungsprogramm möglicherweise eine legitime Neuanordnung vornimmt Dinge aus Gründen der Ausführungseffizienz.
Die Dokumentation bietet drei Möglichkeiten zur Implementierung einer frühen Filterung:
Die Angabe einer Filterbedingung in der WHEN
Klausel garantiert korrekte Ergebnisse, kann jedoch dazu führen, dass mehr Zeilen aus den Quell- und Zieltabellen gelesen und verarbeitet werden, als unbedingt erforderlich sind (wie im ersten Beispiel gezeigt).
Das Aktualisieren über eine Ansicht , die die Filterbedingung enthält, garantiert auch korrekte Ergebnisse (da geänderte Zeilen für das Aktualisieren über die Ansicht zugänglich sein müssen), erfordert jedoch eine dedizierte Ansicht, die den ungeraden Bedingungen für das Aktualisieren von Ansichten entspricht.
Die Verwendung eines allgemeinen Tabellenausdrucks birgt ähnliche Risiken wie das Hinzufügen von Prädikaten zur ON
Klausel, allerdings aus leicht unterschiedlichen Gründen. In vielen Fällen wird es sicher sein, aber es erfordert eine fachmännische Analyse des Ausführungsplans, um dies zu bestätigen (und umfangreiche praktische Tests). Zum Beispiel:
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
;
Dies führt zu korrekten Ergebnissen (nicht wiederholt) mit einem optimaleren Plan:
Der Plan liest nur Zeilen für Kategorie 2 aus der Zieltabelle. Dies ist möglicherweise eine wichtige Überlegung zur Leistung, wenn die Zieltabelle groß ist, aber es ist allzu einfach, dies mit der MERGE
Syntax falsch zu verstehen.
Manchmal ist es einfacher, die MERGE
DML-Operationen als separate zu schreiben . Dieser Ansatz kann sogar eine bessere Leistung erbringen als ein einzelner MERGE
, eine Tatsache, die die Menschen oft überrascht.
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;