sql server 的 merge 是唯一原生 upsert 方案,但必须同时定义 matched 和 not matched 分支,on 条件需规避 null 比较,源数据须为子查询或 values,且依赖索引与约束保障正确性。

MERGE 是 SQL Server 中实现 UPSERT 的唯一原生方案,但它不是“写对就完事”的语法糖——漏掉分支、ON 条件含 NULL、源数据没约束,都会直接报错或行为异常。
SQL Server 的 MERGE 必须同时写 MATCHED 和 NOT MATCHED
和 Oracle 不同,SQL Server 强制要求两个分支都存在。哪怕你业务上只允许更新、禁止插入,也得显式写出 WHEN NOT MATCHED THEN INSERT VALUES ()(哪怕里面是空值或抛错),否则会报:
The MERGE statement attempted to UPDATE or DELETE the same row more than once 或直接语法拒绝。
常见错误写法:
- 只写
WHEN MATCHED THEN UPDATE,漏掉NOT MATCHED -
INSERT后误写成INSERT INTO ... SELECT ...,但没加VALUES子句 - 把
USING写成常量值列表,如USING (@id, @name)—— 必须包装成子查询或VALUES并带列别名,例如USING (VALUES (@id, @name)) AS s(id, name)
ON 条件里不能隐式比较 NULL
ON t.id = s.id 在任一字段为 NULL 时结果恒为 UNKNOWN,导致匹配失败,最终全走 NOT MATCHED 分支 —— 这不是你想要的“按主键 upsert”,而是“所有 NULL 键都被当成新记录插入”。
正确做法是提前过滤或显式处理:
- 在
USING子查询中加WHERE s.id IS NOT NULL - 或改用
ON (t.id = s.id) OR (t.id IS NULL AND s.id IS NULL)(仅当业务真允许 NULL 主键)
更稳妥的方式是:确保参与 ON 的列有 NOT NULL 约束 + 唯一索引,这是 MERGE 正常执行的前提。
源数据必须来自表/子查询,不能直接传参数
SQL Server 不接受 USING @id, @name 这类写法。参数必须被包裹进结果集,常用两种形式:
USING (SELECT @id AS id, @name AS name) AS sUSING (VALUES (@id, @name)) AS s(id, name)
注意:VALUES 方式在批量 upsert 时更高效;而 SELECT 方式便于加 WHERE 过滤或连接其他逻辑。另外,若用临时表或表变量作源(如 #staging),务必确认它有合适的索引,否则 MERGE 可能因找不到高效联接路径而性能骤降。
并发与触发器行为和单条语句不一致
MERGE 是原子语句,但底层可能被优化器拆成多个操作(尤其目标表上有 INSTEAD OF 或 AFTER 触发器时)。这意味着:
- 触发器可能被多次触发(比如先 update 再 insert,两个触发器都跑)
- 如果依赖
@@ROWCOUNT判断影响行数,结果不可靠 —— 它返回的是整个MERGE的总影响行数,不是某一分支的 - 想审计哪行被 insert / update,必须用
OUTPUT子句,例如OUTPUT $action, inserted.*, deleted.*
真正容易被忽略的是:MERGE 的“原子性”只保证语句级事务,不保证语义级幂等。如果你在高并发下反复 upsert 同一条记录,且源数据本身不稳定(比如时间戳每次不同),WHEN MATCHED 分支仍会重复执行 UPDATE —— 这不是 bug,是设计使然。











