根本原因是源数据重复或ON条件不满足一对一映射,导致SQL Server拒绝歧义操作;必须通过源去重(如ROW_NUMBER)、目标建唯一索引、显式加HOLDLOCK锁三方面解决。

根本原因不是并发本身,而是源数据或匹配逻辑不满足 MERGE 的原子性前提——它要求 ON 条件必须能一对一映射,否则直接报错或静默出错。解决方向只有三个:源去重、目标建唯一索引、显式加锁。
源表重复导致 MERGE 报错 “attempted to UPDATE or DELETE the same row more than once”
这是最常见现象,本质是多个 source 行 ON 到同一个 target 行,SQL Server 拒绝这种歧义操作。
- 错误写法:
USING (SELECT id, name FROM staging)—— 若staging里有两条id = 100的记录,就会触发报错 - 正确做法:先用
ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC) = 1去重,再进USING - 别在
USING子句里直接套多层子查询(比如SELECT * FROM (SELECT ..., ROW_NUMBER() ...) x WHERE rn = 1),SQL Server 可能无法优化;优先走临时表:SELECT *, ROW_NUMBER() ... INTO #staging_clean FROM #staging,再CREATE INDEX IX_id ON #staging_clean(id)
ON 条件漏字段或用了函数,导致重复插入
MERGE 不会“智能推导业务唯一性”,它只认 ON 表达式结果。漏字段或函数用错,等于主动放弃唯一约束。
- 错误示例:
ON t.email = s.email—— 若业务实际以tenant_id + email组合唯一,同一邮箱跨租户会被反复插入 - 正确写法:
ON t.tenant_id = s.tenant_id AND t.email = s.email - 禁止在 ON 里用函数:
UPPER(t.email) = UPPER(s.email)会跳过索引,且可能让优化器误估行数;应提前清洗好大小写,或用计算列+索引 - 目标表对应字段必须有
UNIQUE INDEX或主键,否则运行时报错:The MERGE statement attempted to UPDATE or DELETE the same row more than once
高并发下没加 HOLDLOCK,出现幻读和重复插入
READ COMMITTED 隔离级别下,MERGE 的匹配判断过程不被保护,两个并发事务可能同时判定某行“不存在”,然后都执行 INSERT。
- 必须在目标表 alias 后加
WITH (HOLDLOCK),例如:MERGE dbo.orders AS t WITH (HOLDLOCK) - 不要依赖
IF NOT EXISTS+ INSERT 包裹的写法,它天生有竞态窗口,即使加了事务也不够;HOLDLOCK是 SQL Server 对 MERGE 最轻量级的并发保护 - 如果目标表很大且 ON 字段没索引,
HOLDLOCK可能升级为表锁;务必确认ON字段上有索引,否则性能和并发都会崩
MySQL / PostgreSQL 用户注意:没有 MERGE 就别硬套逻辑
MySQL 的 INSERT ON DUPLICATE KEY UPDATE 和 PostgreSQL 的 INSERT ON CONFLICT 行为与 SQL Server MERGE 不同,不能简单替换。
- MySQL 必须先建
UNIQUE KEY,否则ON DUPLICATE KEY UPDATE完全不触发,退化成普通INSERT - PostgreSQL 必须显式指定索引名:
ON CONFLICT (order_no) DO UPDATE SET ...,不能只写ON CONFLICT DO UPDATE,否则不生效 - 三者共通底线:所有防重机制都依赖数据库层唯一约束,前端按钮禁用、后端查重再插,全是无效防线
最容易被忽略的是:MERGE 的原子性只保单条语句,不保跨表一致性。比如订单插入+库存扣减,得靠完整事务 + SERIALIZABLE 隔离级别兜底,不能只靠 MERGE 本身。

















