on子句决定匹配逻辑而非过滤,写错会导致全表误更新;必须确保连接字段在源表和目标表上均唯一,避免类型隐式转换、重复键及索引缺失,否则引发误删、性能退化或不可控行为。

ON子句不是WHERE,写错直接全表误更新
MERGE的ON子句决定“哪些行算匹配”,它不负责过滤,只负责配对。一旦条件写宽了(比如漏掉关键业务约束),WHEN MATCHED THEN UPDATE就会批量作用于所有被误判为“匹配”的行。
- 常见错误:
ON tgt.id = src.id而源数据里src.id有重复,或目标表tgt.id非主键——MERGE会报错或行为不可控 - 更隐蔽的坑:
ON tgt.sku = src.sku AND tgt.warehouse_id = src.warehouse_id写成ON tgt.sku = src.sku,结果跨仓覆盖库存 - 安全做法:始终确保
ON字段组合在源表和目标表上都具备唯一性;若不确定,先用SELECT COUNT(*) FROM src GROUP BY ... HAVING COUNT(*) > 1验重
隐式类型转换让ON失效,却不会报错
当ON两边字段类型不一致(如tgt.user_id INT vs src.user_id VARCHAR),数据库可能自动加CONVERT或CAST,导致索引无法用于定位,也破坏了MERGE所需的确定性匹配逻辑。
- 后果不只是慢:优化器可能跳过唯一性推导,退化为many-to-many模式,内部缓存膨胀、tempdb压力飙升
- 典型现象:执行计划里
Merge Join节点下出现Compute Scalar或Convert,且Ordered="false" - 检查方法:
SELECT system_type_id, user_type_id FROM sys.columns WHERE object_id = OBJECT_ID('table') AND name = 'col'对比两边
NOT MATCHED BY SOURCE容易误删,且难以回滚
WHEN NOT MATCHED BY SOURCE THEN DELETE看着简洁,但它的触发前提是“源中没有对应行”,而这个“对应”完全依赖ON条件的严谨性。一旦ON漏条件或源数据缺失,目标表整批数据就没了。
- 真实案例:ETL任务中源表因上游故障少跑一天,
NOT MATCHED BY SOURCE把当天本该保留的历史快照全删了 - 无事务保护:即使整个MERGE包在事务里,DELETE动作本身不可逆,日志恢复成本高
- 替代方案:改用
UPDATE SET is_active = 0软删,或加前置校验(如EXISTS (SELECT 1 FROM source WHERE ...))
索引建不对,MERGE反而比三条独立语句还慢
MERGE不是银弹。它依赖连接字段上的高效索引支撑匹配扫描,否则会退化为嵌套循环+全表扫描,CPU和I/O双高。
- 必须建:目标表
ON列上的UNIQUE CLUSTERED INDEX(SQL Server)或PRIMARY KEY(PostgreSQL/MySQL);源表对应列上要有UNIQUE索引 - 不能只建普通索引:非唯一索引无法让优化器确认one-to-many,仍会启用tempdb缓存重复键
- 验证点:执行计划中
Merge Join的左右子节点必须是Index Seek或Clustered Index Scan,且属性Ordered="true"
ON条件是否精确表达了业务意义上的“同一行”。它不像JOIN可以靠WHERE兜底,MERGE的每个分支都是原子生效的——错一点,就是整批错。










