over子句本身不直接引发死锁,但配合dml操作、无索引的order by列及并发写入时,会因全表扫描、长锁持有和资源争用,成为死锁链的关键环节。

OVER 子句本身不会直接引发死锁,但配合 UPDATE/DELETE + 窗口函数排序逻辑 + 未覆盖索引时,极易触发长锁持有和资源争用,进而成为死锁链中关键一环。
为什么 OVER 子句会卷入死锁
SQL Server 在执行含 OVER 的 DML(如 UPDATE ... FROM (SELECT ..., ROW_NUMBER() OVER(...)))时,并不“跳过”锁机制。它仍需按执行计划访问基表数据——而窗口函数的 ORDER BY 列若无对应索引,会导致全表扫描或大范围键查找;若该语句又在事务中与其他写操作交叉访问相同数据页/行,就容易形成锁等待闭环。
常见诱因包括:
-
ROW_NUMBER() OVER (ORDER BY created_date)但created_date列没有索引 → 引发大量 KEY 或 PAGE 锁 - 两个并发会话都执行
UPDATE t SET flag = 1 FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY id) rn FROM t WHERE status = 0) x WHERE t.id = x.id AND x.rn → 按不同顺序加锁,互等对方已持锁的行 - 使用
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW的聚合窗口函数 + 大数据量 + 未建合适索引 → 触发长时间共享锁(S),阻塞后续 X 锁请求
用 DMV 快速定位是否是 OVER 相关语句在死锁中
死锁发生后,先查 system_health 中的 xml_deadlock_report,重点看 process 节点下的 inputbuf 或 executionStack:
- 如果看到类似
UPDATE ... FROM (SELECT ..., ROW_NUMBER() OVER (ORDER BY ...)或... RANK() OVER (PARTITION BY ... ORDER BY ...),基本可锁定问题源头 - 注意对比两个死锁进程的
ORDER BY列是否一致:不一致(如一个按id,另一个按ts)是典型循环等待信号 - 检查
resource-list中的objectname和indexname,确认是否集中在同一张表的同一非聚集索引上(说明排序路径高度重叠)
临时验证脚本(从 ring buffer 提取最近死锁中的含 OVER 语句):
SELECT
XEvent.value('(@timestamp)[1]', 'datetime2') AS dt,
XEvent.query('(data/value/deadlock)[1]') AS graph,
XEvent.value('(/event/data/value/deadlock/process-list/process/@inputbuf)[1]', 'nvarchar(4000)') AS inputbuf
FROM (
SELECT CAST(target_data AS XML) AS TargetData
FROM sys.dm_xe_session_targets st
JOIN sys.dm_xe_sessions s ON s.address = st.event_session_address
WHERE s.name = 'system_health' AND st.target_name = 'ring_buffer'
) AS Data
CROSS APPLY TargetData.nodes('//RingBufferTarget/event[@name="xml_deadlock_report"]') AS T(XEvent)
WHERE XEvent.value('(/event/data/value/deadlock/process-list/process/@inputbuf)[1]', 'nvarchar(4000)') LIKE '%OVER%'
如何避免 OVER 子句参与的死锁
核心原则:让窗口函数的排序和定位尽可能走索引,缩短锁持有时间,并统一访问顺序。
- 为
OVER中的ORDER BY列(及PARTITION BY列)创建覆盖索引,例如:CREATE INDEX IX_t_status_id ON t(status, id) INCLUDE (created_date),这样ROW_NUMBER() OVER (PARTITION BY status ORDER BY id)可直接索引扫描 - 避免在事务中嵌套复杂窗口逻辑;能拆成两步(先 SELECT INTO #tmp,再 UPDATE)就拆,减少锁跨度
- 慎用
ROWS BETWEEN/RANGE BETWEEN的滑动窗口,尤其在高并发更新场景;改用更确定的过滤条件替代 - 若必须用
TOP N+OVER分页更新,优先用OFFSET-FETCH替代(SQL Server 2012+),其执行计划更稳定、锁粒度更可控
容易被忽略的细节
很多人以为加了索引就万事大吉,但实际中常踩三个坑:
-
ORDER BY列类型隐式转换(如varchar和nvarchar混用)导致索引失效,窗口排序退化为 SORT 运算符 → 大量内存+锁 - 查询提示
OPTION (RECOMPILE)能帮优化器选对计划,但若语句高频执行,编译开销反而拖慢整体吞吐,得权衡 - 死锁图里显示的“Victim”未必是写法最差的那个会话——有时只是它代价估算更低;真正该优化的是那个反复出现在多个死锁图里的“常驻阻塞者”,哪怕它没被杀掉










