窗口函数本身不慢,慢的是缺失partition by和order by字段的联合索引,导致执行计划出现using temporary; using filesort;rows帧可走索引顺序扫描,range帧易引发重复值重算与性能退化。

窗口函数本身不改变执行计划结构,但会触发临时表和文件排序
MySQL 8.0 解析器在遇到 OVER() 子句时,会强制将整个查询重写为物化中间结果的逻辑。即使你只用 ROW_NUMBER() OVER (ORDER BY id),EXPLAIN 里也大概率出现 Using temporary; Using filesort——这不是警告,是必然行为。
原因在于:窗口计算必须先完成分区与排序,才能逐行派发窗口函数结果。这个“先排序再计算”的阶段无法被传统索引完全规避,尤其当 PARTITION BY 和 ORDER BY 字段未联合索引覆盖时。
- 单字段
ORDER BY create_time+ 无索引 → 必走 filesort -
PARTITION BY dept_id ORDER BY salary DESC→ 需要联合索引(dept_id, salary),缺一不可 - 用了
RANGE帧(如RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)→ 即使有索引,也可能退化为全扫描,因值范围匹配无法利用 B+ 树有序性
ROWS 和 RANGE 帧对执行计划的影响差异极大
ROWS 按物理偏移定义窗口,MySQL 可以用游标顺序扫描 + 计数器维护窗口边界;RANGE 按值语义匹配,必须反复回表或扫描重复值区间,执行计划中常表现为额外的嵌套循环或临时表膨胀。
典型现象:SUM(sales) OVER (ORDER BY sale_date RANGE UNBOUNDED PRECEDING) 在 sale_date 有大量重复值时,EXPLAIN 显示 Using where; Using index 但实际执行慢十倍——因为每遇到一个新日期,它都要把当天所有行拉进来重算一次累计值。
- 安全写法:强制加唯一字段保序,如
ORDER BY sale_date, id ROWS UNBOUNDED PRECEDING - 一旦看到 EXPLAIN 输出里有
Using temporary且没Using filesort,说明窗口帧是ROWS类型且排序已由索引满足 -
RANGE几乎无法避免临时表,除非你确认排序字段值绝对唯一且已建索引
WHERE/HAVING 过滤不了窗口函数结果,导致执行计划虚高
很多人写 WHERE rank ,结果发现执行计划显示扫描全表、临时表巨大——因为窗口函数在 WHERE 之后执行,MySQL 必须先把所有行的 <code>rank 全算出来,再过滤。优化器不会提前剪枝。
真正有效的写法是用 CTE 或子查询封装窗口逻辑,让外层 WHERE 作用于已计算好的结果集:
WITH ranked AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY score DESC) AS rn FROM products ) SELECT * FROM ranked WHERE rn <p>这时 EXPLAIN 会清晰显示:内层物化全部数据,外层走简单全表扫描过滤——虽然仍扫了 100% 行,但至少避免了在窗口计算阶段做无效分支判断。</p>
- 别指望优化器自动下推
WHERE到窗口内层,它做不到 - 如果数据量大,考虑在 CTE 前加
LIMIT或预过滤条件(如WHERE status = 'active'),减少输入行数 - CTE 不是银弹:MySQL 8.0 默认物化 CTE,若多次引用同一 CTE,会重复执行,需加
MATERIALIZED或NOT MATERIALIZED显式控制
索引缺失才是性能瓶颈,不是语法写得不够炫
执行计划里最该盯住的不是“有没有 window function”,而是 “key 列是否命中索引” 和 “Extra 是否含 Using filesort”。90% 的慢窗口查询,问题不在 OVER 怎么写,而在 PARTITION BY a, ORDER BY b 缺少对应联合索引。
例如:COUNT(*) OVER (PARTITION BY user_id ORDER BY created_at),若只有单列索引 user_id 或只有 created_at,MySQL 就得先按 user_id 分组,再对每个分组单独排序——这比全表排序还贵。
- 联合索引必须严格匹配
PARTITION BY前缀 +ORDER BY后缀,顺序不能颠倒 - 如果
ORDER BY是DESC,索引也要声明为INDEX (dept_id, salary DESC)(MySQL 8.0 支持降序索引) -
EXPLAIN FORMAT=TREE比传统 EXPLAIN 更清楚地展示窗口物化步骤,建议优先使用
窗口函数的执行计划不像普通查询那样“越简单越好”。它天然带物化开销,关键在控制这个开销的规模——靠索引把排序压进存储引擎,靠 ROWS 把窗口边界锁死在物理行上,靠 CTE 把过滤动作挪到计算之后。否则,哪怕语法完全正确,执行计划也会默默拖垮整条 SQL。











