Reputation: 1438
I have table OrderLines(OrderID int, LineIndex int, ) and table valued parameter of the same structure defining new order lines for one order.
So if I had the following OrderLines
1000 1 bread
1000 2 milk
1001 1 oil
1001 2 yogurt
1002 1 beef
1002 2 pork
and the following TVP
1001 1 yogurt
I want to get the following OrderLines
1000 1 bread
1000 2 milk
1001 1 yogurt
1002 1 beef
1002 2 pork
I.e. touch rows only for one Order.
So I wrote my query like this
MERGE
[OrderLines] AS [Target]
USING
(
SELECT
[OrderID], [LineIndex], [Data]
FROM
@OrderLines
)
AS [Source] ([OrderID], [LineIndex], [Data])
ON ([Target].[OrderID] = [Source].[OrderID]) AND ([Target].[LineIndex] = [Source].[LineIndex])
WHEN MATCHED THEN
UPDATE
SET
[Target].[Data] = [Source].[Data]
WHEN NOT MATCHED BY TARGET THEN
INSERT
([OrderID], [LineIndex], [Data])
VALUES
([Source].[OrderID], [Source].[LineIndex], [Source].[Data])
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
and it deletes all other (not mentioned) OrderLines for other Orders.
I tried
WHEN NOT MATCHED BY SOURCE AND ([Target].[OrderID] = [Source].[OrderID]) THEN
but got a syntactic error.
How should I rewrite my query?
Upvotes: 12
Views: 9638
Reputation: 77657
Just use the relevant subset of OrderLines
as the target:
WITH AffectedOrderLines AS (
SELECT *
FROM OrderLines
WHERE OrderID IN (SELECT OrderID FROM @OrderLines)
)
MERGE
AffectedOrderLines AS [Target]
USING
(
SELECT
[OrderID], [LineIndex], [Data]
FROM
@OrderLines
)
AS [Source] ([OrderID], [LineIndex], [Data])
ON ([Target].[OrderID] = [Source].[OrderID]) AND ([Target].[LineIndex] = [Source].[LineIndex])
WHEN MATCHED THEN
UPDATE
SET
[Target].[Data] = [Source].[Data]
WHEN NOT MATCHED BY TARGET THEN
INSERT
([OrderID], [LineIndex], [Data])
VALUES
([Source].[OrderID], [Source].[LineIndex], [Source].[Data])
WHEN NOT MATCHED BY SOURCE THEN
DELETE;
And here's a SQL Fiddle to test.
Upvotes: 15
Reputation: 13976
For starters, only columns from the target table can be used in the WHEN NOT MATCHED BY SOURCE
additional merge condition (it's on MSDN).
And I think it's normal that you lose all extra entries from the target table, because they don't match anything in the source.
You should rewrite your query by first deleting the WHEN NOT MATCHED BY SOURCE
clause and then deleting separately extra/unneeded rows.
Then, you need to get all entries that are updated or inserted in the target table by adding:
DECLARE @OutputTable table( OrderId INT, OrderLine INT);
...Your entire MERGE
WHEN NOT MATCHED BY TARGET THEN
INSERT
([OrderID], [LineIndex], [Data])
VALUES
([Source].[OrderID], [Source].[LineIndex], [Source].[Data])
OUTPUT INSERTED.OrderId, INSERTED.LineIndex INTO @OutputTable
Now in @OutputTable
you have all keys that were either updated or entered in the target table (notice the OUTPUT
clause).
You just need now to see which rows from the target table, that only match keys from the @OrderLines
, are not in the @OutputTable' and delete them (so they haven't been updated nor inserted by the
MERGE` statement):
DELETE A
FROM [OrderLines] AS A
INNER JOIN @OrderLines AS B
ON B.OrderId = A.OrderId AND B.LineIndex = A.LineIndex
LEFT OUTER JOIN @OutputTable AS C
ON C.OrderId = A.OrderId AND C.OrderLine = A.LineIndex
WHERE C.OrderId IS NULL AND C.OrderLine IS NULL
What you're doing here (think it's right) is actually what you wanted to delete in the first place. The inner join filters the result set to @OrderLines
(so only rows with those keys) and the left join together with the where clause is doing an anti semi join, to get rows in target table that where not affected by the MERGE
statement (insert or update) but still have keys that are in the source table (@OrderLines
).
Should be right... Let me know after you test it.
You may want to wrap all this (MERGE
+ DELETE
) inside a transaction, if you decide to go with this approach.
Upvotes: 1