MERGE会触发锁升级而非仅行锁,因其默认对目标表扫描范围加意向锁(IX),当预估或实际处理行数超阈值(约5000行)时,SQL Server自动将行锁升级为页锁(PAG)或表锁(TAB);即使只更新一行,若扫描6000行才定位到,仍可能升级。

为什么 MERGE 会触发锁升级而不是行锁
MERGE 在执行时默认对目标表(target_table)的扫描范围加意向锁(IX),如果优化器预估要处理的行数较多,或实际执行中触发了锁数量阈值(通常是 5000 行左右),SQL Server 会自动将大量行锁升级为页锁(PAG)甚至表锁(TAB)。这不是语句写法问题,而是锁管理器的主动行为——哪怕你只 UPDATE 一行,只要扫描了 6000 行才找到它,就可能被升级。
常见现象包括:其他会话对同一表的 INSERT 或 UPDATE 被长时间阻塞,sys.dm_tran_locks 中看到大量 PAG 或 TAB 类型锁,且 resource_description 显示为整页或全表。
- 确保
ON条件列有高效索引(最好是唯一索引或主键),避免全表扫描 - 避免在
USING子句中使用未索引的视图、函数表达式或计算列作为匹配依据 - 用
OPTION (LOOP JOIN)强制嵌套循环连接,可限制扫描范围(但需确认驱动表足够小)
MERGE 的 WHEN MATCHED 和 WHEN NOT MATCHED 实际加锁差异
WHEN MATCHED 执行 UPDATE 时,SQL Server 会对匹配到的**每一行先加 U 锁(更新锁)再升级为 X 锁**;而 WHEN NOT MATCHED 执行 INSERT 时,会在插入位置加 IX + X 锁,但若聚集索引不连续(如 GUID 主键),可能引发页分裂并扩大锁影响范围。
关键点在于:MERGE 不是原子性地“先查再定操作”,而是在连接阶段就锁定所有潜在参与匹配的行。这意味着即使某行最终走的是 INSERT 分支,它在 ON 匹配扫描过程中已被锁住。
- 不要在
WHEN MATCHED中更新非索引列以外的大字段(如varchar(max)),会延长 X 锁持有时间 - 若业务允许,把
INSERT拆出来单独做(用NOT EXISTS+INSERT),可绕过 MERGE 的批量扫描锁 - 测试时用
DBCC TRACEON(1200, -1)查看实际锁申请序列(生产环境慎用)
如何验证当前 MERGE 是否已发生锁升级
直接查动态管理视图比看执行计划更可靠。执行 MERGE 后立即运行:
SELECT resource_type, resource_description,
request_mode, request_status, COUNT(*) cnt
FROM sys.dm_tran_locks
WHERE resource_database_id = DB_ID()
AND resource_associated_entity_id = OBJECT_ID('your_target_table')
GROUP BY resource_type, resource_description, request_mode, request_status;
如果结果中 resource_type 出现大量 PAG 或 TAB,且 request_mode 是 X 或 U,基本确认发生了锁升级。注意:KEY 类型表示行锁(正常),OBJECT 表示整个堆/索引被锁(危险信号)。
- 检查
sys.dm_exec_requests中该会话的wait_type,常见如LCK_M_X长时间等待说明锁冲突严重 - 对比加索引前后
resource_description中的页号(如(1:123456))是否从分散变集中 - 避免依赖 SSMS 的“包含实际执行计划”——它不显示锁升级过程,只反映优化器预期
替代方案:用独立 UPDATE + INSERT 替代 MERGE 的真实代价
拆成两步确实能规避 MERGE 的扫描锁,但引入新问题:两次访问目标表、丢失原子性、需手动处理竞态(比如两次查询之间数据被删又插)。不过在高并发写场景下,这往往是更可控的选择。
典型模式是:
BEGIN TRY UPDATE t SET ... FROM target_table t INNER JOIN #staging s ON t.id = s.id; INSERT INTO target_table (...) SELECT ... FROM #staging s WHERE NOT EXISTS (SELECT 1 FROM target_table t WHERE t.id = s.id); END TRY
这里的关键不是“更快”,而是**锁行为可预测**:UPDATE 只锁匹配行,INSERT 只锁插入页,且两者可分别加索引优化。代价是应用层需容忍部分失败后重试,以及多一次索引查找。
- 务必给
#staging表建临时索引(尤其 ON 列),否则NOT EXISTS可能退化为嵌套循环+全表扫描 - 如果业务要求强一致性,可用
UPDLOCK, HOLDLOCK提示在 UPDATE 里提前锁定范围,但会降低并发度 - 千万避免在事务里先
SELECT再IF EXISTS ... UPDATE/INSERT—— 这种写法在并发下必然出现幻读和丢失更新










