I am trying to use a MERGE
statement to insert or delete rows from a table, but I only want to act on a subset of those rows. The documentation for MERGE
has a pretty strongly worded warning:
It is important to specify only the columns from the target table that are used for matching purposes. That is, specify columns from the target table that are compared to the corresponding column of the source table. Do not attempt to improve query performance by filtering out rows in the target table in the ON clause, such as by specifying AND NOT target_table.column_x = value. Doing so may return unexpected and incorrect results.
but this is exactly what it appears I have to do to make my MERGE
work.
The data I have is a standard many-to-many join table of items to categories (e.g. which items are included in which categories) like so:
CategoryId ItemId
========== ======
1 1
1 2
1 3
2 1
2 3
3 5
3 6
4 5
What I need to do is to effectively replace all rows in a specific category with a new list of items. My initial attempt to do this looks like this:
MERGE INTO CategoryItem AS TARGET
USING (
SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
INSERT ( CategoryId, ItemId )
VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
DELETE ;
This appears to be working in my tests, but I am doing exactly what MSDN explicitly warns me not to do. This makes me concerned that I will run into unexpected problems later on, but I cannot see any other way to make my MERGE
only affect rows with the specific field value (CategoryId = 2
) and ignore rows from other categories.
Is there a "more correct" way to achieve this same result? And what are the "unexpected or incorrect results" that MSDN is warning me about?
Best Answer
The
MERGE
statement has a complex syntax and an even more complex implementation, but essentially the idea is to join two tables, filter down to rows that need to be changed (inserted, updated, or deleted), and then to perform the requested changes. Given the following sample data:Target
Source
The desired outcome is to replace data in the target with data from the source, but only for
CategoryId = 2
. Following the description ofMERGE
given above, we should write a query that joins the source and target on the keys only, and filter rows only in theWHEN
clauses:This gives the following results:
The execution plan is:
Notice both tables are scanned fully. We might think this inefficient, because only rows where
CategoryId = 2
will be affected in the target table. This is where the warnings in Books Online come in. One misguided attempt to optimize to touch only necessary rows in the target is:The logic in the
ON
clause is applied as part of the join. In this case, the join is a full outer join (see this Books Online entry for why). Applying the check for category 2 on the target rows as part of an outer join ultimately results in rows with a different value being deleted (because they do not match the source):The root cause is the same reason predicates behave differently in an outer join
ON
clause than they do if specified in theWHERE
clause. TheMERGE
syntax (and the join implementation depending on the clauses specified) just make it harder to see that this is so.The guidance in Books Online (expanded in the Optimizing Performance entry) offers guidance that will ensure the correct semantic is expressed using
MERGE
syntax, without the user necessarily having to understand all the implementation details, or account for the ways in which the optimizer might legitimately rearrange things for execution efficiency reasons.The documentation offers three potential ways to implement early filtering:
Specifying a filtering condition in the
WHEN
clause guarantees correct results, but may mean that more rows are read and processed from the source and target tables than is strictly necessary (as seen in the first example).Updating through a view that contains the filtering condition also guarantees correct results (since changed rows must be accessible for update through the view) but this does require a dedicated view, and one that follows the odd conditions for updating views.
Using a common table expression carries similar risks to adding predicates to the
ON
clause, but for slightly different reasons. In many cases it will be safe, but it requires expert analysis of the execution plan to confirm this (and extensive practical testing). For example:This produces correct results (not repeated) with a more optimal plan:
The plan only reads rows for category 2 from the target table. This might be an important performance consideration if the target table is large, but it is all too easy to get this wrong using
MERGE
syntax.Sometimes, it is easier to write the
MERGE
as separate DML operations. This approach can even perform better than a singleMERGE
, a fact which often surprises people.