窗口函数不能在WHERE中直接过滤,因其在SELECT阶段计算而WHERE执行更早;正确做法是用子查询或CTE封装后再过滤。

窗口函数不能直接在WHERE里过滤
窗口函数(如 ROW_NUMBER()、RANK()、AVG() OVER)是在 SELECT 阶段计算的,而 WHERE 执行在 SELECT 之前 —— 所以写 WHERE row_num > 1 会报错:column "row_num" does not exist。这不是语法错误,是 SQL 执行顺序决定的硬限制。
正确做法是把带窗口函数的查询包一层子查询或 CTE:
SELECT * FROM ( SELECT id, name, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn FROM students ) t WHERE rn
- 别用
HAVING替代 —— 它只对 GROUP BY 聚合有效,不认窗口别名 - CTE 写法更清晰,但某些旧版 MySQL(
- 子查询必须起别名(比如上面的
t),否则 PostgreSQL/SQL Server 会报错
ORDER BY 在窗口定义和最终结果中要区分清楚
OVER (ORDER BY ...) 控制的是窗口内排序逻辑,比如累计求和或排名依据;而最外层的 ORDER BY 控制的是最终返回行的顺序 —— 两者互不影响,也**不能互相替代**。
常见翻车场景:以为写了 ROW_NUMBER() OVER (ORDER BY created_at) 就不用再写外层 ORDER BY,结果发现返回顺序随机(尤其在 PostgreSQL 中)。
- 如果需要按排名展示,外层必须显式写
ORDER BY rn - 窗口里的
ORDER BY影响计算结果(比如SUM() OVER (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)),漏写会导致逻辑全错 - MySQL 8.0+ 对窗口
ORDER BY要求严格:若未指定,且没有PARTITION BY,可能默认按物理存储序,不可靠
嵌套窗口查询容易触发性能陷阱
多层子查询套窗口函数(比如“先算每人平均分,再按平均分排名,再取前5”)会让优化器难以复用中间结果,特别是数据量大时,可能重复扫描原表。
典型低效写法:
SELECT * FROM ( SELECT name, (SELECT AVG(score) FROM exams e2 WHERE e2.student_id = e1.id) avg_score FROM exams e1 ) t1 JOIN ( SELECT name, RANK() OVER (ORDER BY avg_score DESC) rk FROM (...) t2 ) t3 ON t1.name = t3.name;
- 优先用单层窗口 + CTE 拆解,让数据库有机会物化中间结果(PostgreSQL/SQL Server 支持 CTE inlining 优化,但别依赖)
- 避免在子查询里反复 JOIN 同一张大表;能提前
GROUP BY聚合的,先聚合再开窗 - SQLite 和旧版 MySQL 不支持 CTE,只能靠临时表或应用层分步处理
NULL 值在窗口函数中的行为很具体
窗口函数对 NULL 的处理不是统一跳过,而是取决于函数类型和数据库实现。比如 ROW_NUMBER() 会给 NULL 分配序号,但 RANK() 和 DENSE_RANK() 默认把所有 NULL 视为相等并排在一起(PostgreSQL 行为),而 SQL Server 默认把 NULL 排最前。
如果业务要求“NULL 排最后”,不能只靠 ORDER BY,得显式控制:
ROW_NUMBER() OVER (ORDER BY score DESC NULLS LAST)
-
NULLS FIRST / NULLS LAST是 SQL:2003 标准,PostgreSQL/Oracle 支持,MySQL 8.0+ 支持,SQL Server 不支持(得用CASE WHEN score IS NULL THEN 1 ELSE 0 END, score DESC) -
AVG() OVER会自动忽略 NULL,但COUNT() OVER不会(COUNT(col)忽略 NULL,COUNT(*)不忽略) - 分区键含 NULL 时,有些数据库(如 Hive)会把所有 NULL 归为同一组,有些(如 Presto)则每行 NULL 自成一组
窗口函数本身不难,难的是执行顺序、NULL 边界、数据库方言差异这三块——漏掉任意一个,查出来的数据就可能对不上预期。










