SQL Server MERGE Statement: Pitfalls and a Safer Upsert Pattern
"Insert the row if it does not exist, otherwise update it" is one of the most common tasks in any application. SQL Server's MERGE statement looks like the perfect tool, and it can be, but it has sharp edges that cause duplicate key errors, deadlocks and accidental deletes in busy systems. Here is what to watch for, and a simpler pattern that is safe under load.

How MERGE works
MERGE dbo.Employee AS target
USING (SELECT @EmployeeCode AS EmployeeCode, @FullName AS FullName) AS source
ON target.EmployeeCode = source.EmployeeCode
WHEN MATCHED THEN
UPDATE SET FullName = source.FullName
WHEN NOT MATCHED BY TARGET THEN
INSERT (EmployeeCode, FullName, DepartmentId, CountryCode, HireDate)
VALUES (source.EmployeeCode, source.FullName, 1, 'IN', CAST(GETDATE() AS DATE));It compares a source with a target and runs an update, insert or delete for each row, all in one statement.
Pitfall 1: race conditions
A single MERGE statement is not automatically safe against concurrent calls. Under the default isolation level, two sessions can both check for EmployeeCode = 'E100', both find nothing, and both try to insert it. One of them then fails with a primary key or unique index violation, which typically shows up only under real production load.
If you use MERGE for upserts, add a HOLDLOCK hint (serializable semantics) on the target, so the range stays locked between the check and the write:
MERGE dbo.Employee WITH (HOLDLOCK) AS target
USING ...Pitfall 2: duplicate rows in the source
If the source contains two rows that match the same target row, MERGE fails with an error, because it would update the same row twice. De-duplicate the source first, for example with ROW_NUMBER(), before merging bulk data.
Pitfall 3: WHEN NOT MATCHED BY SOURCE
WHEN NOT MATCHED BY SOURCE THEN DELETE;This deletes every target row that is not in the source. With a full source table that is exactly what you want for a sync. With a filtered or partial source, such as one department's staging data, it deletes everything else in the target. Always filter the target in that clause, or avoid it unless the source is truly complete.
Pitfall 4: the semicolon
A MERGE statement must end with a semicolon, or you get a syntax error. It is a small thing, but it trips people up when they add MERGE to older code that does not use semicolons.
Pitfall 5: complexity and triggers
Triggers fire once per action type (insert, update, delete) for the whole statement, and the plan for a large MERGE with many clauses can be harder to tune than separate statements. For complex batch syncs, separate UPDATE, INSERT and DELETE statements are often easier to read, tune and debug.
A safer single-row upsert pattern
For single-row upserts from an application, this pattern is simple, readable and safe under concurrency:
SET XACT_ABORT ON;
BEGIN TRANSACTION;
UPDATE dbo.Employee WITH (UPDLOCK, SERIALIZABLE)
SET FullName = @FullName
WHERE EmployeeCode = @EmployeeCode;
IF @@ROWCOUNT = 0
BEGIN
INSERT INTO dbo.Employee (EmployeeCode, FullName, DepartmentId, CountryCode, HireDate)
VALUES (@EmployeeCode, @FullName, @DepartmentId, @CountryCode, @HireDate);
END
COMMIT TRANSACTION;- The
UPDLOCK, SERIALIZABLEhints lock the key range even when the row does not exist yet, so a second session waits instead of inserting a duplicate. - When the row exists (the common case in many systems), only one statement runs.
- It needs an index on
EmployeeCodeso the lock covers a small range, not the whole table.
When MERGE is a good choice
- Batch synchronisation from a complete staging table, run by one job at a time.
- ETL processes where its single-statement form makes the logic clearer.
- When you also want the
OUTPUT $actionclause to log what happened to each row.
Quick checklist
- Using
MERGEfor concurrent upserts? AddWITH (HOLDLOCK)on the target. - De-duplicate the source first.
- Be very careful with
WHEN NOT MATCHED BY SOURCE THEN DELETE. - End the statement with a semicolon.
- For single-row upserts, prefer UPDATE-then-INSERT with proper locking.
Related reading:
Comments
Post a Comment