窗口函数是纯计算操作,不修改数据、不持有写锁,仅在读取时短暂持共享锁,依赖索引可实现流式计算、锁范围小且并发安全。

窗口函数本身不持有写锁
窗口函数(如 ROW_NUMBER()、RANK()、LAG())是纯计算型操作,不修改数据,也不触发行级排他锁(X锁)或表级锁。它只在结果集生成阶段读取数据,且多数情况下可复用已有的共享锁(S锁),而 S 锁之间是兼容的——多个并发查询同时执行 SELECT ... OVER() 不会相互阻塞。
常见错误现象:误以为 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) 会像 UPDATE 那样锁住 user_id 分组的所有行;实际上只要底层 SELECT 走索引、不扫描全表,锁只落在实际读取的行上,且事务一结束就释放。
- 窗口函数不改变数据状态,因此不参与锁升级逻辑(比如不会从行锁升级为页锁或表锁)
- 即使在
REPEATABLE READ隔离级别下,它也只依赖当前一致性读快照,不额外申请范围锁(gap lock) - 若外层嵌套了
UPDATE或DELETE并引用窗口结果(如WHERE rn = 1),锁行为由 DML 语句决定,不是窗口函数导致的
执行计划中通常避免物化中间结果
现代数据库(SQL Server 2016+、PostgreSQL、MySQL 8.0+)对窗口函数做了深度优化:只要排序字段有对应索引,引擎倾向于流式计算(streaming aggregate),边读边算,不落临时表、不缓存全部结果集。这意味着锁持有时间极短——只在访问某一行时短暂持 S 锁,而非一次性锁住整个分区数据。
对比嵌套子查询:SELECT * FROM (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t) t2 WHERE rn = 1 在 MySQL 5.7 中可能物化为派生表并锁全表;但等价的 CTE + 窗口写法:WITH ranked AS (SELECT *, ROW_NUMBER() OVER (...) AS rn FROM t WHERE status = 1) SELECT * FROM ranked WHERE rn = 1,条件 status = 1 可以下推,索引有效,锁范围大幅缩小。
- 确保
OVER子句中的ORDER BY字段有索引,否则排序强制磁盘临时表,增加 I/O 和潜在锁等待 - 避免在窗口函数里用非确定性函数(如
GETDATE()、NEWID()),会导致优化器放弃流式路径 - SQL Server 中若使用
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW且未建合适索引,可能引发排序溢出(spill to tempdb),间接加剧资源争用
与普通聚合或自连接相比,并发安全边界更清晰
用窗口函数替代自连接实现“每组最新记录”时(例如找每个用户的最新订单),它天然规避了多表 JOIN 导致的锁交叉风险。传统写法 SELECT o1.* FROM orders o1 LEFT JOIN orders o2 ON o1.user_id = o2.user_id AND o1.created_at 可能因 JOIN 顺序、索引缺失导致大量行被锁定甚至死锁;而 <code>SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn FROM orders) t WHERE rn = 1 的锁只作用于最终输出的那些行,且顺序可控。
- 窗口函数的执行逻辑是单表驱动,没有跨行/跨表的加锁依赖链
- 所有排序和编号动作发生在内存或已排序索引扫描路径上,不引入额外的闩锁(latch)或自旋锁(spinlock)争用
- 在高并发报表场景中,它比反复查
MAX(created_at)子查询 + 关联更稳定——后者每次都要重新评估子查询锁范围
真正容易出问题的地方往往不在窗口函数本身
很多人看到 ROW_NUMBER() 执行慢或引发等待,第一反应是“窗口函数锁多”,其实八成是外层或底层出了问题:比如没走索引导致全表扫描、事务包裹了窗口查询并长时间不提交、或者把窗口结果直接用于 UPDATE ... FROM 却没意识到 DML 才是锁源。
最常被忽略的一点:在 SQL Server 中,如果窗口查询被用在视图或内联表值函数(ITVF)里,而调用方又加了 WITH (NOLOCK) 或其他锁提示,提示不会自动穿透到窗口定义内部——必须显式写在基础 FROM 子句上,否则隔离级别仍按默认(READ COMMITTED)走,该等锁还是等。

















