MERGE在SQL Server 2019中易触发死锁,因其默认以不可预测顺序加锁:可能先锁非聚集索引再回表锁聚集索引,而并发MERGE反向加锁,形成循环等待;需用USING子句中加UPDLOCK+HOLDLOCK强制统一锁序,或拆分为有序INSERT+UPDATE。

为什么MERGE在SQL Server 2019中容易触发死锁
MERGE语句本身不是“Bug”,但在SQL Server 2019(及更早版本)中,它默认以不可预测的锁顺序访问源和目标表,尤其当目标表有多个索引、且WHERE条件未精准命中唯一键时,MERGE可能先获取非聚集索引上的排他锁,再回表请求聚集索引锁——而另一个并发MERGE可能按相反顺序加锁,形成循环等待。这与“多表更新顺序不一致”的经典死锁场景完全一致。
用UPDLOCK + HOLDLOCK强制锁顺序
最直接有效的缓解方式,是在MERGE的USING子句中对源数据显式加锁,让所有并发执行都按同一路径申请锁:
MERGE target_table AS t USING (SELECT id, value FROM source_data WITH (UPDLOCK, HOLDLOCK)) AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.value = s.value WHEN NOT MATCHED THEN INSERT (id, value) VALUES (s.id, s.value);
关键点:
-
UPDLOCK防止其他会话对同一行做UPDATE或MERGE,避免“读-改-写”窗口期 -
HOLDLOCK等价于SERIALIZABLE,把扫描范围锁住,防止幻读引发的锁重排 - 必须加在
USING的子查询上,而不是target_table;加在目标表上无效且可能扩大锁粒度 - 如果源是临时表或CTE,需先物化(例如插入#temp),再对其加锁——CTE无法直接加
WITH提示
替代方案:拆成INSERT + UPDATE并统一访问顺序
当业务允许时,显式拆分比MERGE更可控。重点在于确保所有路径都按相同物理顺序访问目标行:
- 用
SELECT ... ORDER BY id驱动后续操作(即使逻辑无需排序,也强制SQL Server按该顺序加锁) -
UPDATE和INSERT分开执行,中间不混杂其他DML - 对目标表的
WHERE条件字段必须有覆盖索引,避免全表扫描导致锁行数爆炸 - 若涉及多列匹配(如
WHERE a=@a AND b=@b),索引必须是(a, b)或(b, a)之一,并在所有代码中严格保持列顺序一致
SQL Server 2019是否支持优化锁定?
不支持。OPTIMIZE_FOR_SEQUENTIAL_KEY和优化锁定(TID/LAQ)都是SQL Server 2022+ 和 Azure SQL 才引入的特性。SQL Server 2019仍使用传统锁管理器,MERGE的锁行为不会因数据库选项自动改善。即使启用READ_COMMITTED_SNAPSHOT,它只缓解读阻塞,对MERGE中的写冲突无实质帮助——因为MERGE的匹配判断和变更操作仍需真实行锁。
真正容易被忽略的是:哪怕你加了UPDLOCK,如果源数据来自未索引视图或函数,SQL Server仍可能选择低效执行计划,绕过你的锁意图。务必检查实际执行计划里的Seek/Scan类型和锁资源提示。

















