merge的on子句不能写join,须将join逻辑提前至using子句;on条件需覆盖全部业务唯一键、禁用函数;update/insert字段须显式对齐并处理null;批量操作须分批、事务、错误捕获及源去重。

ON子句里不能直接写JOIN,得用USING构造源数据集
MERGE语法本身不支持在ON后面跟JOIN,常见错误是试图写成 ON t.id = s.id JOIN s2 ON s.id = s2.ref_id——这会直接报错。正确做法是把JOIN逻辑提前放到USING子句里,让源数据先“准备好”。比如要同步订单主表和扩展属性,就得先SELECT出带JOIN结果的临时集,再作为源传给MERGE。
实操建议:
- 源数据必须是单个表、视图、CTE或子查询,不能是多个独立表并列
- 推荐用CTE封装JOIN逻辑,语义清晰且便于调试:
WITH JoinedSource AS (SELECT t1.*, t2.status_code FROM StagingOrders t1 LEFT JOIN OrderStatus t2 ON t1.status_id = t2.id) - 确保JOIN后的结果中,用于匹配的字段(如
order_no)不出现NULL或重复值,否则MERGE会报“The MERGE statement attempted to update or delete the same row more than once”
ON条件必须覆盖全部业务唯一键,别只靠主键
很多人把ON t.id = s.id当万能解,但业务上真正判重的往往是组合字段。比如多租户系统里,email + tenant_id才是唯一标识,而id可能是自增或为空。如果ON只写id,就会漏更新、重复插入,甚至因违反唯一约束失败。
实操建议:
- 显式写出所有参与业务判重的字段:
ON t.email = s.email AND t.tenant_id = s.tenant_id - 禁止在ON中用函数(如
UPPER(t.email)),目标表对应列必须有可走索引的等值条件 - 如果源数据来自JOIN,确认JOIN后
email和tenant_id字段确实来自非计算列、且未被GROUP BY或聚合抹平
UPDATE和INSERT字段映射必须显式对齐,NULL处理不能偷懒
WHEN MATCHED THEN UPDATE SET 和 WHEN NOT MATCHED THEN INSERT 共享同一份源数据,但字段顺序、默认值、NULL语义极易错位。漏写一列、顺序不对、或没处理NULL,轻则数据丢失,重则语句执行失败。
实操建议:
- INSERT必须明确列出列名:
INSERT (name, email, updated_at) VALUES (s.name, s.email, GETDATE()),不能依赖源字段顺序 - UPDATE中若源字段可能为NULL且需保留原值,写成
name = ISNULL(s.name, t.name),而不是name = s.name - 时间戳字段(如
updated_at)建议在UPDATE分支里强制赋值GETDATE(),MERGE默认绕过AFTER触发器,不会自动更新
批量同步必须分批+事务+错误捕获,别一把梭
一次性对几千行跑MERGE,大概率触发锁升级、日志暴涨或死锁。尤其当源表是JOIN结果时,执行计划更难预测,资源消耗翻倍。
实操建议:
- 每批控制在20–50行,用
OFFSET / FETCH NEXT分页,避免游标开销 - 每个MERGE必须包在
BEGIN TRY / BEGIN CATCH里,捕获ERROR_MESSAGE()并记录失败批次 - 禁用隐式事务;显式
BEGIN TRAN+COMMIT,并在CATCH里IF @@TRANCOUNT > 0 ROLLBACK
最常被忽略的是源数据去重——哪怕JOIN结果看起来干净,也要在USING前加ROW_NUMBER() OVER (PARTITION BY email, tenant_id ORDER BY updated_at DESC)兜底,否则一条重复就全批崩。










