🔰 1. Einfaches Beispiel – UPSERT (INSERT / UPDATE ohne DELETE)
EINSTIEGER
📌 Ziel: Synchronisieren einer Zieltabelle mit einer Quelle. Existiert der Datensatz bereits (MATCHED), wird er aktualisiert, ansonsten eingefügt (NOT MATCHED).
Löschungen werden ignoriert – das klassische "UPSERT".
-- Quelltabelle (z.B. Staging) und ZieltabelleCREATE TABLE #SourceProducts (ProductID INT, ProductName VARCHAR(50), Price DECIMAL(10,2));
CREATE TABLE #TargetProducts (ProductID INT PRIMARY KEY, ProductName VARCHAR(50), Price DECIMAL(10,2));
INSERT INTO #SourceProducts VALUES (1, 'Laptop', 999.99), (2, 'Maus', 19.99), (3, 'Tastatur', 49.99);
INSERT INTO #TargetProducts VALUES (1, 'Laptop', 899.99); -- alter Preis-- MERGE: UPSERT ohne DELETEMERGE INTO #TargetProducts AS T
USING #SourceProducts AS S
ON T.ProductID = S.ProductID
WHEN MATCHED THENUPDATE SET T.ProductName = S.ProductName, T.Price = S.Price
WHEN NOT MATCHED THENINSERT (ProductID, ProductName, Price)
VALUES (S.ProductID, S.ProductName, S.Price);
-- Ergebnis: Produkt 1 wurde geupdatet, 2+3 wurden eingefügt.SELECT * FROM #TargetProducts;
💡 Erklärung: Die MERGE-Anweisung vergleicht die Quell- und Zieltabelle über die ON-Bedingung. Bei Übereinstimmung UPDATE, sonst INSERT. Ideal für Datenübernahme aus Staging-Tabellen ohne Datenverlust.
📌 Ziel: Die Zieltabelle soll exakt dem Stand der Quelle entsprechen. Datensätze, die in der Quelle nicht mehr vorkommen, werden gelöscht.
TRUNCATE TABLE #TargetProducts;
INSERT INTO #TargetProducts VALUES (1, 'Laptop', 899.99), (99, 'Altes Produkt', 1.99);
TRUNCATE TABLE #SourceProducts;
INSERT INTO #SourceProducts VALUES (1, 'Laptop', 1099.99), (2, 'Maus', 24.99);
-- MERGE mit DELETE für Quelle-verwaiste ZeilenMERGE INTO #TargetProducts AS T
USING #SourceProducts AS S
ON T.ProductID = S.ProductID
WHEN MATCHED THENUPDATE SET T.ProductName = S.ProductName, T.Price = S.Price
WHEN NOT MATCHED BY TARGET THENINSERT (ProductID, ProductName, Price) VALUES (S.ProductID, S.ProductName, S.Price)
WHEN NOT MATCHED BY SOURCE THENDELETE;
SELECT * FROM #TargetProducts;
-- Erwartetes Ergebnis: Nur ID 1 (upgedatet) und ID 2 (neu). ID 99 wurde gelöscht.
🧹 Erklärung: Mit NOT MATCHED BY SOURCE werden alle Zeilen in der Zieltabelle gelöscht, die in der Quelle nicht existieren. Dadurch entsteht eine 1:1-Übernahme – perfekt für Dimensions- oder Referenzdaten.
🧩 3. Komplexe Quelle – CTE oder Subquery als Datengrundlage
ERWEITERT
📌 Ziel: Die Quelle muss keine permanente Tabelle sein – sie kann eine CTE (Common Table Expression) oder eine Subquery sein. Das ermöglicht dynamische Aggregationen oder Joins direkt im Merge.
-- Beispiel: Umgesetzte Bestellungen aggregieren (Source = CTE)CREATE TABLE #SalesTarget (ProductID INT, TotalSold INT);
INSERT INTO #SalesTarget VALUES (1, 10), (2, 5);
-- Temporäre BestelldetailsCREATE TABLE #Orders (OrderID INT, ProductID INT, Quantity INT);
INSERT INTO #Orders VALUES (101,1,3),(102,1,2),(103,2,5),(104,3,7);
-- MERGE mit CTE als QuelleWITH SourceAgg AS (
SELECT ProductID, SUM(Quantity) AS TotalSold
FROM #Orders
GROUP BY ProductID
)
MERGE INTO #SalesTarget AS T
USING SourceAgg AS S
ON T.ProductID = S.ProductID
WHEN MATCHED THENUPDATE SET T.TotalSold = S.TotalSold
WHEN NOT MATCHED BY TARGET THENINSERT (ProductID, TotalSold) VALUES (S.ProductID, S.TotalSold)
WHEN NOT MATCHED BY SOURCE THENDELETE;
SELECT * FROM #SalesTarget ORDER BY ProductID;
-- Ergebnis: ID 1 -> 5, ID 2 -> 5, ID 3 -> 7 (neu), ID ? alte gelöscht
⚙️ Erklärung: Die CTE SourceAgg aggregiert die Bestellungen. Diese virtuelle Tabelle wird dann als Quelle für MERGE verwendet. So kannst du beliebig komplexe Logiken in die Quelle einfließen lassen – ohne permanente Zwischentabellen.
📊 4. Record Counting – Anzahl INSERT, UPDATE, DELETE ermitteln
PROFI
📌 Ziel: Mit der OUTPUT-Klausel kann man nachvollziehen, welche Aktion pro Zeile ausgeführt wurde. Die Pseudospalte $action liefert 'INSERT', 'UPDATE' oder 'DELETE'. Zusammen mit einer temporären Tabelle oder einem Zähler erhält man granulare Statistiken.
-- Vorbereitung TestdatenCREATE TABLE #Src (ID INT, Wert VARCHAR(20));
CREATE TABLE #Dst (ID INT PRIMARY KEY, Wert VARCHAR(20));
INSERT INTO #Src VALUES (1,'A'),(2,'B'),(3,'C');
INSERT INTO #Dst VALUES (1,'A_old'),(4,'X'),(5,'Y');
-- MERGE mit OUTPUT $action in eine Tischvariable (oder TempTable)DECLARE @ChangeLog TABLE (ActionType VARCHAR(10), OldID INT, NewID INT);
MERGE INTO #Dst AS T
USING #Src AS S
ON T.ID = S.ID
WHEN MATCHED THENUPDATE SET T.Wert = S.Wert
WHEN NOT MATCHED BY TARGET THENINSERT (ID, Wert) VALUES (S.ID, S.Wert)
WHEN NOT MATCHED BY SOURCE THENDELETEOUTPUT$actionAS AktionsTyp,
CASE WHEN$action = 'UPDATE'THENDELETED.ID ENDAS OldID,
CASE WHEN$action IN ('INSERT','UPDATE') THENINSERTED.ID ENDAS NewID
INTO @ChangeLog;
-- Auswerten der ZählerSELECT
ActionType,
COUNT(*) AS Anzahl
FROM @ChangeLog
GROUP BY ActionType;
-- Detail anzeigen (optional)SELECT * FROM @ChangeLog;
-- Exakte Zählung über die TischvariableDECLARE @InsertCount INT, @UpdateCount INT, @DeleteCount INT;
SELECT
@InsertCount = COUNT(CASE WHEN ActionType = 'INSERT'THEN 1 END),
@UpdateCount = COUNT(CASE WHEN ActionType = 'UPDATE'THEN 1 END),
@DeleteCount = COUNT(CASE WHEN ActionType = 'DELETE'THEN 1 END)
FROM @ChangeLog;
PRINT'INSERT: ' + CAST(@InsertCount ASVARCHAR) +
' | UPDATE: ' + CAST(@UpdateCount ASVARCHAR) +
' | DELETE: ' + CAST(@DeleteCount ASVARCHAR);
-- Ausgabe: INSERT: 1 | UPDATE: 1 | DELETE: 2
📈 Erklärung: Die OUTPUT $action Klausel fängt jede durchgeführte Aktion ab. Wir speichern sie in einer Tabellenvariable (@ChangeLog). Danach kann man aggregiert auswerten, wie viele INSERTs, UPDATEs und DELETEs tatsächlich stattgefunden haben – perfekt für Logging, Audit oder Meldungen in ETL-Prozessen.
💡 Tipp für den Alltag: Die MERGE Anweisung ist sehr mächtig, aber es gibt ein paar Fallstricke (z.B. tricky bei der Quellkardinalität). Nutzen Sie den OUTPUT Trick immer dann, wenn Sie eine Rückmeldung über die Anzahl der Änderungen benötigen – auch in gespeicherten Prozeduren bestens geeignet.
🎯 Bonus: Vollständiges Beispiel mit Fehlerbehandlung & COUNT als Resultset
EXPERTE
📌 Ziel: Kompakte gespeicherte Prozedur, die eine MERGE Synchronisation durchführt und eine Ergebnismenge mit den Zählern zurückgibt.
CREATE OR ALTER PROCEDURE dbo.Sync_ProductCatalog
ASBEGINSET NOCOUNT ON;
DECLARE @Changes TABLE (Act VARCHAR(10));
MERGE INTO dbo.Products AS T
USING dbo.StagingProducts AS S
ON T.ProductID = S.ProductID
WHEN MATCHED AND (T.Name != S.Name OR T.Price != S.Price) THENUPDATE SET T.Name = S.Name, T.Price = S.Price, T.ModifiedDate = GETDATE()
WHEN NOT MATCHED BY TARGET THENINSERT (ProductID, Name, Price, CreatedDate)
VALUES (S.ProductID, S.Name, S.Price, GETDATE())
WHEN NOT MATCHED BY SOURCE AND T.IsActive = 1 THENUPDATE SET T.IsActive = 0, T.DeletedDate = GETDATE()
OUTPUT$actionINTO @Changes;
WITH Counts AS (
SELECT Act, COUNT(*) AS Anzahl
FROM @Changes
GROUP BY Act
)
SELECTISNULL((SELECT Anzahl FROM Counts WHERE Act = 'INSERT'), 0) AS Inserted,
ISNULL((SELECT Anzahl FROM Counts WHERE Act = 'UPDATE'), 0) AS Updated,
ISNULL((SELECT Anzahl FROM Counts WHERE Act = 'DELETE'), 0) AS Deleted;
END;
GO-- Aufruf liefert eine Zeile mit drei Spalten: Inserted, Updated, Deleted
EXEC dbo.Sync_ProductCatalog;
🚀 Erklärung: Dieses Prozedurbeispiel zeigt, wie man MERGE in der Praxis verwendet – mit zusätzlichen Bedingungen (AND in der MATCHED-Klausel, um unnötige Updates zu vermeiden) und weichem Löschen (Soft-Delete). Die OUTPUT‑Tabelle liefert die exakten Zähler, die am Ende als Resultset zurückgegeben werden.
⚡ Wichtige Hinweise zu MERGE: Achten Sie auf eindeutige Quellzuordnungen – wenn die USING-Quelle mehrere Zeilen mit dem gleichen Schlüssel liefert, kann MERGE mehrdeutig werden (Fehler!). Verwenden Sie nach Möglichkeit ROW_NUMBER() in der Quelle, um Duplikate zu beseitigen. Der OUTPUT $action Trick ist die beste Methode, um einsicht in die durchgeführten Operationen zu bekommen. Seit SQL Server 2016 ist MERGE auch bei System-Versioned Temporal Tables verfügbar.
Die hier bereitgestellten Skripte und Codebeispiele wurden mit größtmöglicher Sorgfalt geprüft und getestet. Dennoch übernehmen wir keinerlei Gewähr für die Richtigkeit, Vollständigkeit oder Fehlerfreiheit der Inhalte. Es kann nicht ausgeschlossen werden, dass die Skripte unbeabsichtigte Fehler enthalten oder unter bestimmten Umgebungsbedingungen nicht wie erwartet funktionieren.
Der Einsatz der Skripte erfolgt ausschließlich auf eigene Gefahr. Vor der Ausführung in einer Produktivumgebung wird dringend empfohlen, die Skripte in einer geeigneten Testumgebung gründlich zu validieren und an die individuellen Gegebenheiten anzupassen. Der Autor haftet nicht für direkte oder indirekte Schäden, die durch die Verwendung oder Nichtverwendbarkeit der bereitgestellten Informationen entstehen.