How to delete from source using MERGE command in SQL Server 2008?

In our case, we wanted to use MERGE to synchronize our internal database with an outside source of a different structure. Automated CASCADE settings were not an option because we enjoy many cyclical relationships and, really, we don't like that kind of cheap power in the hands of disgruntled staffers. We can't delete parent rows before their child rows are gone.

All of this is done with lightning fast MERGEs that use Table Value Parameters. They provide, by far, the best performance with obscenely low app memory overhead.

Combining scattered advice for the MERGE of Orders data...

CREATE PROCEDURE MyOrderMerge @SourceValues [MyOrderSqlUserType] READONLY
AS
BEGIN
    DECLARE @LiveRows TABLE (MergeAction VARCHAR(20), OrderId INT);
    DECLARE @DeleteCount INT;

    SET @DeleteCount = 0;

    MERGE INTO [Order] AS [target]

        USING   (   SELECT  sv.OrderNumber,
                    c.CustomerId,
                    st.ShipTypeId
                    sv.OrderDate,
                    sv.IsPriority

                    FROM @SourceValues sv
                        JOIN [Customer] c ON sv.[CustomerName] = c.[CustomerName]
                        JOIN [ShipType] st ON ...
            ) AS [stream]
            ON [stream].[OrderNumber] = [target].[SourceOrderNumber]

        WHEN MATCHED THEN
            UPDATE
                ...

        WHEN NOT MATCHED BY TARGET THEN
            INSERT
                ---

        -- Keep a tally of all active source records
        -- SQL Server's "INSERTED." prefix encompases both INSERTed and UPDATEd rows <insert very bad words here>
        OUTPUT $action, INSERTED.[OrderId] INTO @LiveRows

    ;   -- MERGE has ended

    -- Delete child OrderItem rows before parent Order rows

    DELETE FROM [OrderItem]
        FROM [OrderItem] oi
            -- Delete the Order Items that no longer exist at the source
            LEFT JOIN @LiveRows lr ON oi.[OrderId] = lr.[OrderId]
        WHERE lr.OrderId IS NULL
    ;

    SET @DeleteCount = @DeleteCount + @@ROWCOUNT;

    -- Delete parent Order rows that no longer have child Order Item rows

    DELETE FROM [Order]
        FROM [Order] o
            -- Delete the Orders that no longer exist at the source
            LEFT JOIN @LiveRows lr ON o.[OrderId] = lr.[OrderId]
        WHERE lr.OrderId IS NULL
    ;

    SET @DeleteCount = @DeleteCount + @@ROWCOUNT;

    SELECT MergeAction, COUNT(*) AS ActionCount FROM @LiveRows GROUP BY MergeAction
    UNION
    SELECT 'DELETE' AS MergeAction, @DeleteCount AS ActionCount
    ;
END

Everything is done in one sweet loop-dee-loop streamed round trip and highly optimized on key indexes. Even though internal primary key values are unknown from the source, the MERGE operation makes them available to the DELETE operations.

The Customer MERGE uses a different @LiveRows TABLE structure, consequentially a different OUTPUT statement and different DELETE statements...

CREATE PROCEDURE MyCustomerMerge @SourceValues [MyCustomerSqlUserType] READONLY
AS
BEGIN
    DECLARE @LiveRows TABLE (MergeAction VARCHAR(20), CustomerId INT);
    DECLARE @DeleteCount INT;

    SET @DeleteCount = 0;

    MERGE INTO [Customer] AS [target]

        ...

        OUTPUT $action, INSERTED.[CustomerId] INTO @LiveRows

    ;   -- MERGE has ended

    -- Delete child OrderItem rows before parent Order rows

    DELETE FROM [OrderItem]
        FROM [OrderItem] oi
            JOIN [Order] o ON oi.[OrderId] = o.[OrderId]
                -- Delete the Order Items that no longer exist at the source
                LEFT JOIN @LiveRows lr ON o.[CustomerId] = lr.[CustomerId]
        WHERE lr.CustomerId IS NULL
    ;

    SET @DeleteCount = @DeleteCount + @@ROWCOUNT;

    -- Delete child Order rows before parent Customer rows

    DELETE FROM [Order]
        FROM [Order] o
            -- Delete the Orders that no longer exist at the source
            LEFT JOIN @LiveRows lr ON o.[CustomerId] = lr.[CustomerId]
        WHERE lr.CustomerId IS NULL
    ;

    SET @DeleteCount = @DeleteCount + @@ROWCOUNT;

    -- Delete parent Customer rows that no longer have child Order or grandchild Order Item rows

    DELETE FROM [Customer]
        FROM [Customer] c
            -- Delete the Customers that no longer exist at the source
            LEFT JOIN @LiveRows lr ON c.[CustomerId] = lr.[CustomerId]
        WHERE lr.CustomerId IS NULL
    ;

    SET @DeleteCount = @DeleteCount + @@ROWCOUNT;

    SELECT MergeAction, COUNT(*) AS ActionCount FROM @LiveRows GROUP BY MergeAction
    UNION
    SELECT 'DELETE' AS MergeAction, @DeleteCount AS ActionCount
    ;
END

Setup and maintenance is a bit of a pain - but so worth the efficiencies reaped.


You can use the output clause to capture the modified/inserted rows to a table variable and use that with a delete statement after the merge.

DECLARE @T TABLE(EmployeeID INT);

MERGE Target1 AS T
USING Source1 AS S
ON (T.EmployeeID = S.EmployeeID) 
WHEN NOT MATCHED BY TARGET AND S.EmployeeName LIKE 'S%' 
    THEN INSERT(EmployeeID, EmployeeName) VALUES(S.EmployeeID, S.EmployeeName)
WHEN MATCHED 
    THEN UPDATE SET T.EmployeeName = S.EmployeeName
WHEN NOT MATCHED BY SOURCE AND T.EmployeeName LIKE 'S%'
    THEN DELETE  
OUTPUT S.EmployeeID INTO @T;

DELETE Source1
WHERE EmployeeID in (SELECT EmployeeID
                     FROM @T);


Nice Reponse, but your code will delete the row from your destination table, here's an exemple in wich you can delete the rows from your source destination without affecting your target table :

if OBJECT_ID('audit.tmp1') IS NOT NULL
    DROP TABLE audit.tmp1   

select *
into audit.tmp1
from 
(
select 1 id, 'aa' nom, convert(date,'2014-01-01') as dd UNION ALL 
select 2 id, 'bb' nom, convert(date,'2013-07-12') as dd UNION ALL 
select 3 id, 'cc' nom, convert(date,'2012-08-21') as dd UNION ALL 
select 4 id, 'dd' nom, convert(date,'2011-11-15') as dd UNION ALL 
select 5 id, 'ee' nom, convert(date,'2010-05-16') as dd ) T


if OBJECT_ID('audit.tmp2') IS NOT NULL
DROP TABLE audit.tmp2   

select *
into audit.tmp2
from 
(
select 1 id, 'aAa' nom, convert(date,'2014-01-14') as dd UNION ALL 
select 2 id, 'bbB' nom, convert(date,'2013-06-13') as dd UNION ALL 
select 4 id, 'dDD' nom, convert(date,'2012-11-05') as dd UNION ALL 
select 6 id, 'FFf' nom, convert(date,'2014-01-12') as dd) T


SELECT * FROM audit.tmp1 order by 1
SELECT * FROM audit.tmp2 order by 1


DECLARE @T TABLE(ID INT);

MERGE audit.tmp2 WITH (HOLDLOCK) AS T
USING (SELECT * FROM audit.tmp1 WHERE nom <> 'dd') AS S
ON (T.id = S.id)
WHEN NOT MATCHED BY TARGET
THEN INSERT(id, nom, dd) VALUES(S.id, S.nom, S.dd)
WHEN MATCHED 
THEN UPDATE SET T.nom = S.nom, T.dd = S.dd
WHEN NOT MATCHED BY SOURCE
THEN UPDATE SET T.id = T.id OUTPUT S.id INTO @T;

DELETE tmp1
FROM audit.tmp1
INNER JOIN
@T AS DEL
    ON DEL.id = tmp1 .id


SELECT * FROM audit.tmp1 ORDER BY 1
SELECT * FROM audit.tmp2 ORDER BY 1

I hope this will help you.