MERGE在SQL Server 2019中并发易死锁,因其非原子操作且索引访问顺序不确定,导致循环等待;必须在USING子句加UPDLOCK+HOLDLOCK,显式写出WHEN MATCHED/NOT MATCHED,并确保ON条件基于非NULL唯一键及覆盖索引。

MERGE 在 SQL Server 2019 中默认不安全,不加锁提示几乎必然在并发场景下触发死锁。
为什么 MERGE 在 SQL Server 2019 中一并发就死锁
MERGE 不是原子操作的“魔法语句”,它内部会按不确定顺序访问目标表的多个索引:可能先锁非聚集索引键,再回表锁聚集索引行;另一个并发 MERGE 却反着来——先锁聚集索引,再试图锁非聚集索引。这就构成经典的循环等待(KEY: 7:72057594038321152 (b5a7e9c9e3d9) 这类 waitresource 值常同时出现在两个 victim 的死锁图里)。这种行为在 SQL Server 2019 中完全无法通过数据库配置自动规避,READ_COMMITTED_SNAPSHOT 对写冲突无效,OPTIMIZE_FOR_SEQUENTIAL_KEY 是 2022+ 才有的特性。
必须在 USING 子句中加 UPDLOCK + HOLDLOCK
这是最直接、最可控的修复方式,强制所有并发事务按同一路径申请锁:
-
UPDLOCK防止其他会话对源数据行做 UPDATE/MERGE,堵住“读-改-写”窗口期 -
HOLDLOCK等价于SERIALIZABLE,把整个扫描范围锁住,避免幻读导致锁重排 - 必须加在
USING的子查询上,例如(SELECT id, value FROM source_data WITH (UPDLOCK, HOLDLOCK));加在target_table上不仅无效,还可能扩大锁粒度到整张表 - 如果源是 CTE 或表变量,得先物化进临时表(如
SELECT ... INTO #temp FROM ...),再对#temp加锁——CTE 不支持WITH锁提示
WHEN MATCHED 和 WHEN NOT MATCHED 都不能省
SQL Server 强制要求这两个分支必须显式写出,漏掉任一个会直接报错或静默失败:
- 报错:
Incorrect syntax near the keyword 'MERGE'(语法错误)或更隐蔽的The MERGE statement attempted to UPDATE or DELETE the same row more than once - 即使业务逻辑只允许插入(比如日志归档),也得补空更新,例如
WHEN MATCHED THEN UPDATE SET @dummy = @dummy - 匹配条件(
ON子句)必须基于非 NULL 的唯一键,否则 SQL Server 可能对多行做重复匹配判断,加剧锁竞争
替代方案:拆成 INSERT + UPDATE 并统一访问顺序
当业务允许且对原子性要求不是极端严格时,显式拆分比 MERGE 更易控、更易调优:
- 用
SELECT id FROM source_data ORDER BY id驱动后续操作,强制 SQL Server 按物理顺序加锁 - UPDATE 和 INSERT 分开执行,中间不混杂其他 DML;尤其避免在 UPDATE 后、INSERT 前查表或发消息
- 目标表的
WHERE字段必须有覆盖索引,比如ON t.id = s.id就要建INDEX IX_t_id ON target_table(id),否则全表扫描会锁成千上万行 - 若匹配条件含多列(如
a=@a AND b=@b),索引必须是(a, b)或(b, a)之一,且所有代码中列顺序必须严格一致
真正容易被忽略的是:死锁往往发生在毫秒级竞争点,不是靠加个 WAITFOR DELAY 就能复现;生产环境必须依赖 system_health 环形缓冲区抓实时死锁图,光看阻塞会话或图形执行计划的“警告图标”会漏掉关键线索。










