T-SQL MERGE – Synchronisation mit INSERT, UPDATE, DELETE (mit Output Counting)

Einfügen, Aktualisieren, Löschen in einem Durchlauf – Praktische Beispiele mit Record Counting

🔰 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 Zieltabelle 
CREATE 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 DELETE 
MERGE INTO #TargetProducts AS T 
USING #SourceProducts AS S 
ON T.ProductID = S.ProductID 
WHEN MATCHED THEN 
UPDATE SET T.ProductName = S.ProductName, T.Price = S.Price 
WHEN NOT MATCHED THEN 
INSERT (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.

⚡ 2. Vollständige Synchronisation – inklusive DELETE

FORTGESCHRITTEN
📌 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 Zeilen 
MERGE INTO #TargetProducts AS T 
USING #SourceProducts AS S 
ON T.ProductID = S.ProductID 
WHEN MATCHED THEN 
UPDATE SET T.ProductName = S.ProductName, T.Price = S.Price 
WHEN NOT MATCHED BY TARGET THEN 
INSERT (ProductID, ProductName, Price) VALUES (S.ProductID, S.ProductName, S.Price) 
WHEN NOT MATCHED BY SOURCE THEN 
DELETE; 
 
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 Bestelldetails 
CREATE 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 Quelle 
WITH 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 THEN 
UPDATE SET T.TotalSold = S.TotalSold 
WHEN NOT MATCHED BY TARGET THEN 
INSERT (ProductID, TotalSold) VALUES (S.ProductID, S.TotalSold) 
WHEN NOT MATCHED BY SOURCE THEN 
DELETE; 
 
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 Testdaten 
CREATE 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 THEN 
UPDATE SET T.Wert = S.Wert 
WHEN NOT MATCHED BY TARGET THEN 
INSERT (ID, Wert) VALUES (S.ID, S.Wert) 
WHEN NOT MATCHED BY SOURCE THEN 
DELETE 
OUTPUT $action AS AktionsTyp, 
CASE WHEN $action = 'UPDATE' THEN DELETED.ID END AS OldID, 
CASE WHEN $action IN ('INSERT','UPDATE') THEN INSERTED.ID END AS NewID 
INTO @ChangeLog; 
 
-- Auswerten der Zähler 
SELECT  
ActionType, 
COUNT(*) AS Anzahl 
FROM @ChangeLog 
GROUP BY ActionType; 
 
-- Detail anzeigen (optional) 
SELECT * FROM @ChangeLog; 
 
-- Exakte Zählung über die Tischvariable 
DECLARE @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 AS VARCHAR) + 
' | UPDATE: ' + CAST(@UpdateCount AS VARCHAR) + 
' | DELETE: ' + CAST(@DeleteCount AS VARCHAR); 
-- 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 
AS 
BEGIN 
SET 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) THEN 
UPDATE SET T.Name = S.Name, T.Price = S.Price, T.ModifiedDate = GETDATE() 
WHEN NOT MATCHED BY TARGET THEN 
INSERT (ProductID, Name, Price, CreatedDate) 
VALUES (S.ProductID, S.Name, S.Price, GETDATE()) 
WHEN NOT MATCHED BY SOURCE AND T.IsActive = 1 THEN 
UPDATE SET T.IsActive = 0, T.DeletedDate = GETDATE() 
OUTPUT $action INTO @Changes; 
 
WITH Counts AS ( 
SELECT Act, COUNT(*) AS Anzahl 
FROM @Changes 
GROUP BY Act 
) 
SELECT  
ISNULL((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.

📌 Alle Beispiele sind gültiger T‑SQL Code für SQL Server 2017+, Azure SQL Database und SQL Server 2022.
MERGE vereint INSERT, UPDATE und DELETE in einer einzigen Anweisung – inklusive präziser Zählung mit $action.

© 2025 · T‑SQL MERGE Deep Dive · Layout angelehnt an powershelldba.de