MERGE语句ON条件必须覆盖全部业务唯一键、源数据须严格去重、高并发下必须加HOLDLOCK提示,三者缺一不可,否则导致重复插入、错行更新或整批失败。

ON条件必须覆盖全部业务唯一键
MERGE的匹配逻辑完全由ON子句决定,它不是“先查再判”,而是一次性划分MATCHED/NOT MATCHED数据流。漏掉任一业务判重字段,就会导致重复插入或更新错行。
- 错误写法:
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)会跳过索引,且优化器可能误估行数 - 目标表对应字段必须有唯一索引(主键或唯一约束),否则运行时报错:
The MERGE statement attempted to UPDATE or DELETE the same row more than once
源数据必须严格去重,否则直接报错
MERGE要求源数据对ON字段严格唯一,否则内部无法确定某一行该更新哪条目标记录。这不是数据质量问题,而是语句结构被SQL Server拒绝。
- 典型错误信息:
The MERGE statement attempted to UPDATE or DELETE the same row more than once - 不能直接用
GROUP BY后进USING,得套一层子查询:USING (SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY key_col ORDER BY updated_at DESC) rn FROM source) x WHERE rn = 1) AS s - 临时表
#staging或表变量@staging是最稳妥的源载体;裸值如USING (@id, @name)非法,必须用USING (VALUES (@id, @name)) AS s(id, name)
UPDATE和INSERT字段映射要各自独立校验
WHEN MATCHED THEN UPDATE SET和WHEN NOT MATCHED THEN INSERT (...) VALUES (...)共享同一个源数据集,但字段顺序、NULL处理、默认值逻辑互不影响,容易静默出错。
- 常见陷阱:
updated_at允许NULL,源字段是空字符串或GETDATE()表达式,却直接写SET updated_at = s.updated_at,结果把NULL写进去覆盖了原值 - 安全写法:
SET updated_at = ISNULL(NULLIF(s.updated_at, ''), GETDATE())或明确补默认值 -
INSERT的VALUES列表必须跟括号内列顺序严格一致;有DEFAULT约束的字段若不显式写DEFAULT,会报错 - 时间戳类字段建议在
UPDATE分支里统一设为GETDATE(),别依赖触发器——MERGE绕过某些AFTER触发器
高并发场景必须加HOLDLOCK提示
SQL Server默认READ COMMITTED隔离级别不足以保护MERGE的匹配判断过程。多个并发执行时,幻读会导致重复插入或丢失更新。
- 错误写法:
MERGE INTO users AS t USING ...——没锁提示,高并发下大概率翻车 - 正确写法:
MERGE INTO users WITH (HOLDLOCK) AS t USING ...,等价于SERIALIZABLE,强制串行化整个操作 -
HOLDLOCK会延长锁持有时间,若批量数据处理跨分钟级,要考虑阻塞影响;临时表的匹配列也建议建索引(尤其数据量 > 10k) -
MERGE必须同时包含WHEN MATCHED和WHEN NOT MATCHED分支,缺一不可,否则语法报错
实际用起来最常卡住的地方,不是语法写不对,而是ON条件没想全、源数据没去重、锁提示没加——这三个点只要漏一个,轻则数据错乱,重则整批失败。

















