Wie lösche ich nur verwandte Datensätze in einem MERGE mit mehreren Schlüsseln in SQL Server?

7

Angenommen, Sie haben so etwas:

Quelltabelle (Variable):

Values (
  LeftId INT NOT NULL,
  RightId INT NOT NULL,
  CustomValue varchar(100) NULL
)

Zieltabelle:

Mapping (
  LeftId INT NOT NULL,
  RightId INT NOT NULL,
  CustomValue varchar(100) NULL
)

Ich mag verschmelzen Valuesin Targetden folgenden Regeln:

  • Match on source.LeftId = target.LeftId AND source.RightId = target.RightId
    • Wenn das Ziel übereinstimmt, aktualisieren Sie es CustomValue
    • Wenn das Ziel nicht übereinstimmt, fügen Sie es ein
  • Löschen Sie alle nicht übereinstimmenden Werte im Ziel, die mit a LeftIdin der Quelle übereinstimmen , dh löschen Sie nur Datensätze, die sich auf die LefIds der Zusammenführung beziehen .

(Diese letzte Regel ist schwer zu beschreiben, sorry!)

Zum Beispiel:

Quelle:

1, 10, foo
1, 11, foo

Ziel:

1, 10, bar
1, 12, foo
2, 20, car

Ergebnis zusammenführen:

Ergebnisziel:

1, 10, foo (updated)
1, 11, foo (inserted)
1, 12, foo (deleted)
2, 20, car (unchanged)

Damit...

Folgendes habe ich bisher, das kümmert sich um updateund insert:

MERGE Mapping AS target
USING (SELECT LeftId, RightId, CustomValue FROM @Values) 
  AS source (LeftId, RightId, CustomValue)
  ON target.LeftId = source.LeftId
    AND target.RightId = source.RightId
WHEN NOT MATCHED THEN
  INSERT (LeftId, RightId, CustomValue)
  VALUES (source.LeftId, source.RightId, source.CustomValue)
WHEN MATCHED THEN
  UPDATE SET
    CustomValue = source.CustomValue;

Wie mache ich den deleteTeil meiner Regel?

Michael Haren
quelle
Es gibt keinen Zweig "übereinstimmend in einer Spalte, aber nicht in der anderen", und die Regel "nicht übereinstimmend", die Sie hinzufügen möchten, scheint auf einer eindeutigen Suche nach den LeftID-Werten in @Values ​​zu beruhen. Warum führen Sie diese Operation nicht in einer zweiten Anweisung aus, anstatt zu versuchen, sie in MERGE einzuschleusen? Der Schutz von Parallelität / Race-Bedingungen ist in MERGE ohnehin ohne explizites HOLDLOCK nicht garantiert. Wenn Sie mehrere Anweisungen erstellen, können Sie mit einer Wrapping-Transaktion denselben Effekt erzielen.
Aaron Bertrand
1
@ AaronBertrand, es scheint der richtige Weg zu sein. Ich denke, ich kann damit den gewünschten Effekt erzielen, aber wie Sie sagen, ist dies möglicherweise nicht angemessen:WHEN NOT MATCHED BY source AND EXISTS(SELECT * FROM @Values M WHERE M.LeftId = target.LeftId) THEN DELETE;
Michael Haren

Antworten:

6

Dies ist die separate DELETEOperation, an die ich gedacht habe:

DELETE m
FROM dbo.Mapping AS m
WHERE EXISTS 
  (SELECT 1 FROM @Values WHERE LeftID = m.LeftID)
AND NOT EXISTS 
  (SELECT 1 FROM @Values WHERE LeftID = m.LeftID AND RightID = m.RightID);

Wie ich hier skizziere , NOT EXISTSübertrifft das LEFT JOIN / NULLMuster bei einem linken Anti-Semi-Join häufig das Muster (aber Sie sollten es immer testen).

Sie sind sich nicht sicher, ob Ihr übergeordnetes Ziel Klarheit oder Leistung ist. Daher können nur Sie beurteilen, ob dies für Ihre Anforderungen besser funktioniert als die NOT MATCHED BY sourceOption. Sie müssen die Pläne qualitativ und die Pläne und / oder Laufzeitmetriken quantitativ betrachten, um sicher zu sein.

Wenn Sie erwarten, dass Ihr MERGEBefehl Sie vor Rennbedingungen schützt, die bei mehreren unabhängigen Anweisungen auftreten würden, sollten Sie sicherstellen, dass dies der Fall ist, indem Sie ihn in Folgendes ändern:

MERGE dbo.Mapping WITH (HOLDLOCK) AS target

(Aus Dan Guzmans Blogbeitrag .)

Persönlich würde ich all dies ohne tun MERGE, da es unter anderem ungelöste Fehler gibt . Und Paul White scheint auch separate DML-Anweisungen zu empfehlen .

Und hier ist , warum ich ein Schema Präfix hinzugefügt: Sie sollten immer von Objekten nach Schema verweisen, beim Erstellen, zu beeinflussen, etc .

Aaron Bertrand
quelle
9

Sie können die zu berücksichtigenden Zeilen aus der Zieltabelle in einem CTE herausfiltern und den CTE als Ziel für die Zusammenführung verwenden.

WITH T AS
(
  SELECT M.LeftId, 
         M.RightId, 
         M.CustomValue
  FROM @Mappings AS M
  WHERE EXISTS (SELECT *
                FROM @Values AS V
                WHERE M.LeftId = V.LeftId) 
)
MERGE T
USING @Values AS S
ON T.LeftId = S.LeftId and
   T.RightId = S.RightId
WHEN NOT MATCHED BY TARGET THEN
  INSERT (LeftId, RightId, CustomValue) 
  VALUES (S.LeftId, S.RightId, S.CustomValue)
WHEN MATCHED THEN
  UPDATE SET CustomValue = S.CustomValue
WHEN NOT MATCHED BY SOURCE THEN
  DELETE
;
Mikael Eriksson
quelle
+1 schöne Problemumgehung (Wald für die Bäume) und gute Abkürzung der Quelle.
Aaron Bertrand
2
Die Verwendung eines CTE hierfür wird anscheinend in BOL gewarnt, obwohl mir nicht klar ist, wann genau es sich anders als in einer Ansicht verhalten könnte.
Martin Smith
@ MartinSmith Ist das nicht ein bisschen seltsam? Ich würde denken, dass die Ansicht von CTE auf die gleiche Weise in die Abfrage erweitert wird. Anscheinend gibt es einen Unterschied in der Art und Weise, wie sie behandelt werden.
Mikael Eriksson
@MikaelEriksson - Ich hatte sie vor dem Lesen auch als austauschbar angesehen.
Martin Smith
@MartinSmith BOL beschreibt Best Practices, damit Sie beim Schreiben der Merge-Anweisung keine dummen Fehler machen. Sobald Sie feststellen, dass die on-Klausel eine vollständige äußere Verknüpfung ist, verstehen Sie, warum seltsame Dinge passieren, wenn Sie nach Konstanten in der on-Klausel filtern. Gleiches gilt für den CTE. Es kann nichts tun, worum Sie es nicht bitten, aber es ist schwer zu erkennen, in welchen Fällen Sie es vermasselt haben. Was als Problem mit dem CTE beschrieben wird, ist, wenn Sie Zeilen aus dem Ziel herausfiltern, die mit der Quelle übereinstimmen und ein Einfügen verhindert hätten.
Mikael Eriksson
3

Sie können die WHEN NOT MATCHED BY SOURCEKlausel verwenden und eine zusätzliche Bedingung wie folgt angeben:

SQL Fiddle

MS SQL Server 2008 Schema-Setup :

CREATE TABLE dbo.Vals (
  LeftId INT NOT NULL,
  RightId INT NOT NULL,
  CustomValue varchar(100) NULL
);

CREATE TABLE dbo.Mapping (
  LeftId INT NOT NULL,
  RightId INT NOT NULL,
  CustomValue varchar(100) NULL
);

INSERT INTO dbo.Vals(LeftId,RightId,CustomValue)
VALUES(1, 10, 'foo10'),(1, 11, 'foo11');

INSERT INTO dbo.Mapping(LeftId,RightId,CustomValue)
VALUES(1, 10, 'bar'),(1, 12, 'foo'),(2, 20, 'car');

Abfrage 1 :

MERGE dbo.Mapping WITH(HOLDLOCK) AS target
USING (SELECT LeftId, RightId, CustomValue FROM dbo.Vals) 
  AS source (LeftId, RightId, CustomValue)
  ON target.LeftId = source.LeftId
    AND target.RightId = source.RightId
WHEN NOT MATCHED THEN
  INSERT (LeftId, RightId, CustomValue)
  VALUES (source.LeftId, source.RightId, source.CustomValue)
WHEN MATCHED THEN
  UPDATE SET
    CustomValue = source.CustomValue


WHEN NOT MATCHED BY SOURCE AND EXISTS(SELECT 1 FROM dbo.Vals iVals WHERE target.LeftId = iVals.LeftId) THEN
  DELETE



OUTPUT $action AS Action,
       INSERTED.LeftId AS INS_LeftId,INSERTED.RightId AS INS_RightId,INSERTED.CustomValue AS INS_Val,
       DELETED.LeftId AS DEL_LeftId,DELETED.RightId AS DEL_RightId,DELETED.CustomValue AS DEL_Val;

Ergebnisse :

| ACTION | INS_LEFTID | INS_RIGHTID | INS_VAL | DEL_LEFTID | DEL_RIGHTID | DEL_VAL |
------------------------------------------------------------------------------------
| INSERT |          1 |          11 |   foo11 |     (null) |      (null) |  (null) |
| UPDATE |          1 |          10 |   foo10 |          1 |          10 |     bar |
| DELETE |     (null) |      (null) |  (null) |          1 |          12 |     foo |

Abfrage 2 :

SELECT * FROM dbo.Mapping;

Ergebnisse :

| LEFTID | RIGHTID | CUSTOMVALUE |
----------------------------------
|      1 |      10 |       foo10 |
|      2 |      20 |         car |
|      1 |      11 |       foo11 |

Ich habe der MERGEAnweisung die Ausgabeklausel hinzugefügt, um zu zeigen, welche Aktion für jede Zeile ausgeführt wurde.

Wie andere kommentiert haben, müssen Sie auch den WITH(HOLDLOCK)Hinweis auf der Zieltabelle angeben, um Rennbedingungen zu vermeiden.

Sebastian Meine
quelle
1

Folgendes habe ich mir ausgedacht. Jedes Feedback wird geschätzt!

-- Testdaten:

DECLARE @Values   TABLE(LeftId INT, RightId INT, CustomValue VARCHAR(100))
DECLARE @Mappings TABLE(LeftId INT, RightId INT, CustomValue VARCHAR(100))

-- the incoming values
INSERT INTO @Values   VALUES (1, 10, 'bar2'), (1, 11, 'foo')

-- the existing table
INSERT INTO @Mappings VALUES (1, 10, 'bar'),  (1, 12, 'foo'), (2, 20, 'car')

- Option 1: Behandeln Sie das deleteTeil separat:

DELETE M
FROM @Mappings M
JOIN (SELECT DISTINCT LeftId FROM @Values) DistinctLeftIds ON M.LeftId = DistinctLeftIds.LeftId 
LEFT JOIN @Values V ON M.LeftId = V.LeftId AND M.RightId = V.RightId
WHERE V.LeftId IS NULL

MERGE @Mappings AS target
USING (SELECT LeftId, RightId, CustomValue FROM @Values) 
  AS source (LeftId, RightId, CustomValue)
  ON target.LeftId = source.LeftId
    AND target.RightId = source.RightId
WHEN NOT MATCHED THEN
  INSERT (LeftId, RightId, CustomValue)
  VALUES (source.LeftId, source.RightId, source.CustomValue)
WHEN MATCHED THEN
  UPDATE SET
    CustomValue = source.CustomValue;

- Option 2: Tun Sie es (umständlich?) In der MERGEAnweisung:

MERGE @Mappings AS target
USING (SELECT LeftId, RightId, CustomValue FROM @Values) 
  AS source (LeftId, RightId, CustomValue)
  ON target.LeftId = source.LeftId
    AND target.RightId = source.RightId
WHEN NOT MATCHED THEN
  INSERT (LeftId, RightId, CustomValue)
  VALUES (source.LeftId, source.RightId, source.CustomValue)
WHEN MATCHED THEN
  UPDATE SET
    CustomValue = source.CustomValue;
WHEN NOT MATCHED BY source 
    AND EXISTS(SELECT * FROM @Values M WHERE M.LeftId = target.LeftId) THEN
  DELETE;

- Ergebnisse überprüfen:

SELECT * FROM @Mappings ORDER BY LeftId, RightId
Michael Haren
quelle