MERGE eine Teilmenge der Zieltabelle

71

Ich versuche, mit einer MERGEAnweisung Zeilen aus einer Tabelle einzufügen oder zu löschen, möchte jedoch nur auf eine Teilmenge dieser Zeilen reagieren. Die Dokumentation für MERGEhat eine ziemlich starke Warnung:

Es ist wichtig, nur die Spalten aus der Zieltabelle anzugeben, die für Abgleichszwecke verwendet werden. Geben Sie also Spalten aus der Zieltabelle an, die mit der entsprechenden Spalte der Quellentabelle verglichen werden. Versuchen Sie nicht, die Abfrageleistung zu verbessern, indem Sie die Zeilen in der Zieltabelle in der ON-Klausel herausfiltern, z. B. durch Angabe von AND NOT target_table.column_x = value. Dies kann zu unerwarteten und falschen Ergebnissen führen.

aber genau das muss ich anscheinend tun, um meine MERGEArbeit zu machen .

Die Daten, die ich habe, sind eine standardmäßige Viele-zu-Viele-Verknüpfungstabelle von Elementen mit Kategorien (z. B. welche Elemente in welchen Kategorien enthalten sind).

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

Was ich tun muss, ist effektiv alle Zeilen in einer bestimmten Kategorie durch eine neue Liste von Elementen zu ersetzen. Mein erster Versuch dazu sieht folgendermaßen aus:

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 ;

Dies scheint in meinen Tests zu funktionieren, aber ich tue genau das, wovor MSDN mich ausdrücklich warnt. Das macht mich besorgt, dass ich später auf unerwartete Probleme stoße, aber ich kann keine andere Möglichkeit finden, meine MERGEZeilen nur mit dem spezifischen Feldwert ( CategoryId = 2) zu beeinflussen und Zeilen aus anderen Kategorien zu ignorieren.

Gibt es einen "korrekteren" Weg, um dasselbe Ergebnis zu erzielen? Und vor welchen "unerwarteten oder falschen Ergebnissen" warnt mich MSDN?

KutuluMike
quelle
Ja, die Dokumentation wäre nützlicher, wenn sie ein konkretes Beispiel für "unerwartete und falsche Ergebnisse" hätte.
AK
3
@AlexKuznetsov Hier gibt es ein Beispiel .
Paul White
@SQLKiwi danke für den Link - IMO wäre die Dokumentation viel besser, wenn sie von der Originalseite verwiesen würde.
AK
1
@AlexKuznetsov Einverstanden. Leider hat die BOL-Reorganisation für 2012 unter anderem das gebrochen. Es wurde in der R2-Dokumentation von 2008 recht gut verlinkt.
Paul White

Antworten:

103

Die MERGEAnweisung 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 MERGEobigen 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 WHENKlauseln 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: Plan zusammenführen

Beachten Sie, dass beide Tabellen vollständig gescannt werden. Wir könnten dies für ineffizient halten, da nur Zeilen in der CategoryId = 2Zieltabelle 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 ONKlausel 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- ONKlausel anders verhalten als in der WHEREKlausel angegeben. Die MERGESyntax (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 MERGESyntax 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 WHENKlausel 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 ONKlausel, 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:

Plan 2 zusammenführen

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 MERGESyntax falsch zu verstehen.

Manchmal ist es einfacher, die MERGEDML-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;
Paul White
quelle
Ich weiß, dass dies eine sehr alte Frage ist ... aber jede Möglichkeit, die Sie erläutern können: "Die Verwendung eines allgemeinen Tabellenausdrucks birgt ähnliche Risiken wie das Hinzufügen von Prädikaten zur ON-Klausel, jedoch aus etwas anderen Gründen." Ich weiß, BOL hat auch eine ähnlich vage Warnung "Diese Methode ähnelt der Angabe zusätzlicher Suchkriterien in der ON-Klausel und kann zu falschen Ergebnissen führen. Wir empfehlen, dass Sie diese Methode vermeiden ...". Die CTE-Methode scheint meinen Anwendungsfall zu lösen, ich frage mich jedoch, ob es ein Szenario gibt, das ich nicht in Betracht ziehe.
Henry Lee