窗口函数并发变慢的根本原因不是锁争用,而是资源挤占:多并发查询同时申请内存做排序、分组和流式聚合,导致work_mem耗尽、频繁落盘(spill)、磁盘I/O突增及GC压力,表现为WindowAgg节点耗时飙升而非死锁。

窗口函数本身不加写锁,高并发下不会因锁争用拖慢整体性能——但内存、排序和执行计划设计不当,会让它成为新的瓶颈点。
为什么并发时窗口函数会变慢,而不是被锁住?
根本原因不是锁,而是资源挤占:多个并发查询同时触发窗口计算,各自申请内存做排序、分组、流式聚合,容易把 work_mem 或 Spark 的 spark.sql.windowExec.buffer.in.memory.threshold 吃光,引发频繁落盘(spill)或 GC 压力。现象是查询响应时间波动大、WindowAgg 节点耗时飙升、磁盘 I/O 突增,而非报死锁或阻塞等待。
- PostgreSQL 中,即使启用了并行扫描,
ROW_NUMBER() OVER (ORDER BY x)仍默认单线程执行——有序性要求让它无法拆分 - Spark 里每个分区的窗口缓冲区独立驻留内存,高并发+大分区 = 内存爆炸,
OOMKilled风险远高于锁冲突 - MySQL 8.0 对
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW有优化,但若ORDER BY字段无索引,照样全表排序+临时表,CPU 直接拉满
PARTITION BY 字段选错,比 SQL 写错更致命
这不是语法错误,日志里完全不报,但会导致所有并发请求都挤在极少数 partition 上——比如按 status(只有 3 个值)分区,99% 的数据落到 status = 1 这一个桶里,单个窗口要处理上百万行,内存溢出、排序溢写全来了。
- 优先用高基数字段:如
user_id、order_id,让压力自然摊开 - 复合索引必须匹配顺序:
PARTITION BY a, b ORDER BY c→ 索引应为(a, b, c),只建(a)没用 - 避免表达式分区:
PARTITION BY DATE(created_at)必须配函数索引,否则每次执行都现场计算+排序
怎么让多个窗口共用一次排序?
写三个 ROW_NUMBER()、LAG()、SUM() OVER (),如果各自带一遍 ORDER BY ts DESC,数据库大概率会跑三次排序——尤其在 PostgreSQL 和 SQL Server 中,除非你显式复用。
- PostgreSQL / SQL Server 支持命名
WINDOW w AS (PARTITION BY user_id ORDER BY ts DESC),后续全用OVER w - MySQL 8.0 不支持命名 WINDOW,必须确保所有窗口的
PARTITION BY和ORDER BY字段完全一致、顺序相同,才可能触发内部排序缓存 - 别在
ORDER BY里用GETDATE()或NEWID():非确定性函数直接让流式路径失效,退化成物化+全排序
真正该盯住的执行计划信号
别只看“有没有窗口函数”,要看 WindowAgg 节点下面挂了什么、占了多少耗时、有没有 spill。
-
Rows Removed by Filter高 → 外层 WHERE 没下推,窗口算了一堆最后被过滤掉,先加WHERE缩小输入集 -
Shared Hit Blocks低、Read Blocks高 → 缓存没命中,检查索引覆盖是否完整 - 出现
Temp File或Spill to disk→ 立刻调work_mem(PG)或spark.sql.windowExec.buffer.spill.threshold(Spark) -
Workers Launched: 0在WindowAgg上 → 即使表支持并行,带ORDER BY的窗口也卡死单线程,别指望自动并行
并发下的窗口函数性能,本质上是个资源调度问题:它不抢锁,但抢内存、抢 CPU、抢磁盘带宽。最容易被忽略的,是把 PARTITION BY 当成业务逻辑随意写,而不评估其对物理执行分布的影响——这比调参重要十倍。


















