on条件必须覆盖全部业务唯一键,不能只写主键;须用and连接所有判重列(如email+tenant_id),禁用函数;源数据须用cte+row_number()去重;update/insert字段需显式对齐并处理null;时间戳须显式赋值;大批量操作须分批、显式事务及错误捕获。

ON条件必须覆盖全部业务唯一键,不能只写主键
很多人把MERGE当INSERT ... ON DUPLICATE KEY用,在ON里只写t.id = s.id。如果业务上真正判重的是email和tenant_id组合,而id为空或自增未生成,这条MERGE就会漏更新、重复插入,甚至报错The MERGE statement attempted to update or delete the same row more than once。
正确做法是把所有用于业务去重的列都放进ON,例如:ON t.email = s.email AND t.tenant_id = s.tenant_id。
- 禁止在
ON中用函数(如UPPER(t.email)),否则目标表索引失效,执行计划可能退化为全表扫描 - 如果源是视图或CTE,确保
ON引用的列已明确定义且非计算列 - 若目标表没有对应组合索引,
MERGE性能会断崖式下降
源数据必须提前去重,不能依赖应用层“保证不重复”
MERGE遇到两条email='a@b.com'的源记录,会直接报错并中止执行。存储过程要自己兜底,不能假设上游数据干净。
推荐在USING子句前用CTE封装去重逻辑:
WITH CleanSource AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY email, tenant_id ORDER BY updated_at DESC) AS rn
FROM #staging_users
)
MERGE INTO dbo.users AS tgt
USING CleanSource AS src ON tgt.email = src.email AND tgt.tenant_id = src.tenant_id
WHEN MATCHED THEN UPDATE SET ...
WHEN NOT MATCHED THEN INSERT ...
- 去重必须基于完整业务键,不是单列
-
ORDER BY要明确优先级(比如取最新updated_at的那条) - 别用
DISTINCT代替ROW_NUMBER()——它无法处理字段值全等但业务含义不同的情况
UPDATE和INSERT字段必须显式对齐,NULL处理不能偷懒
WHEN MATCHED THEN UPDATE SET和WHEN NOT MATCHED THEN INSERT VALUES()共享同一份源数据,但字段映射、默认值、NULL语义极易错位。
常见错误包括:
-
INSERT的VALUES()顺序跟INSERT (col1, col2)列列表不一致 - 漏掉有
DEFAULT的列,又没写DEFAULT关键字,导致报错 -
UPDATE分支中写col = s.col,结果源字段为NULL时把原值清空了;应改用col = ISNULL(s.col, t.col) - 时间戳字段(如
updated_at)没在UPDATE分支里显式赋值GETDATE(),导致字段不变且绕过AFTER触发器
批量同步必须分批 + 显式事务 + 错误捕获
一次性对几千行跑MERGE,轻则日志暴涨,重则锁升级成表级死锁。动态拼SQL容易,稳住执行环境难。
关键控制点:
- 每批最多处理 20–50 行(不是20–50张表),用
OFFSET / FETCH或游标分页 - 每个
MERGE必须包在BEGIN TRY / BEGIN CATCH里,捕获后记录失败表名和ERROR_MESSAGE() - 禁用隐式事务:
SET IMPLICIT_TRANSACTIONS OFF,所有操作用BEGIN TRAN / COMMIT / ROLLBACK显式控制 - 目标表联接列建议建唯一聚集索引,源表对应列建唯一非聚集索引——否则
MERGE可能反复扫描
真正麻烦的从来不是语法拼对,而是ON条件是否穷尽业务唯一性、源数据是否干净、以及出错时能不能准确定位到哪一行。










