窗口函数在group by之后执行,作用于group by产生的聚合行而非原始明细行,其order by必须引用group by输出列或确定性表达式,不可用于where/on中,正确用法是子查询或cte先物化分组结果再开窗。

窗口函数在GROUP BY之后执行,但作用于未被GROUP BY压缩的原始行
窗口函数不会在GROUP BY产生的聚合行上计算,而是作用于GROUP BY前的中间结果集——也就是WHERE和HAVING筛选后、但尚未被GROUP BY“折叠”掉明细的那批数据。但注意:这个“中间结果集”不是原始表,而是SELECT子句中明确保留的列(含聚合表达式)所构成的逻辑行集。
常见错误现象:SELECT dept, AVG(salary), ROW_NUMBER() OVER (ORDER BY AVG(salary)) FROM emp GROUP BY dept 这条语句在多数数据库(如PostgreSQL、SQL Server)中会报错,因为ROW_NUMBER()试图对AVG(salary)排序,而该值是GROUP BY后的聚合结果,不属于“可开窗的行集”。窗口函数要求其ORDER BY子句引用的列必须来自GROUP BY输出的列或其确定性表达式,且不能是“非确定性聚合别名”。
- 正确做法是先用子查询或CTE把GROUP BY结果物化出来,再在其上开窗;例如:
WITH dept_agg AS ( SELECT dept, AVG(salary) AS avg_sal FROM emp GROUP BY dept ) SELECT *, ROW_NUMBER() OVER (ORDER BY avg_sal) AS rn FROM dept_agg;
- MySQL 8.0+ 允许在GROUP BY后直接写窗口函数,但仅限于引用SELECT列表中已出现的列或别名(且该别名必须是确定性表达式),本质仍是先完成GROUP BY,再对GROUP BY输出的每一行开窗
- 不要误以为
PARTITION BY dept能“在分组内再分区”——GROUP BY已经把数据按dept压成一行了,窗口函数再PARTITION BY dept只是对单行做“1行窗口”,毫无意义
为什么不能在WHERE或ON里引用窗口函数结果
因为SQL逻辑执行顺序中,WHERE在FROM→WHERE→GROUP BY→HAVING→SELECT→ORDER BY→LIMIT这一链条的最前端,而窗口函数位于SELECT阶段内部、且严格晚于GROUP BY和HAVING。所以WHERE rank() = 1或ON t1.id = t2.id AND t2.rn = 1这类写法必然失败。
典型错误信息:Window function is not allowed in WHERE clause(PostgreSQL)、Invalid use of window function(SQL Server)。
- 替代方案只有两种:外层包装子查询/CTE,然后在外部WHERE中过滤
rn列 - JOIN场景下,把窗口计算放在子查询中作为驱动表,例如:
SELECT t1.*, t2.top_score FROM emp t1 JOIN ( SELECT dept, MAX(score) AS top_score FROM exam GROUP BY dept ) t2 ON t1.dept = t2.dept;
而不是试图在ON里算ROW_NUMBER() - 某些引擎(如Spark SQL)支持LATERAL JOIN + 窗口函数子查询,但兼容性差,不建议作为通用解法
GROUP BY + 窗口函数混合使用的唯一安全模式
真正能同时出现GROUP BY和窗口函数的合法场景,是“先聚合、再基于聚合结果开窗”,即窗口函数作用对象是GROUP BY的输出行,而非原始明细。此时窗口函数的输入行数 = GROUP BY分组数,不再是原表行数。
使用场景举例:各销售区域季度销售额汇总后,计算每个区域在年度内的累计占比;或按产品类目统计销量后,给类目按销量排名。
- 必须显式写出GROUP BY后的所有非聚合列,并确保窗口函数的
PARTITION BY和ORDER BY只引用这些列或其确定性衍生(如CASE WHEN) - 帧子句(
ROWS BETWEEN ...)仍有效,但意义变为“在分组结果集上滑动”,例如SUM(total_sales) OVER (ORDER BY quarter ROWS UNBOUNDED PRECEDING)是对季度序列做累加 - 性能影响明显:GROUP BY本身已消耗资源,再在其结果上开窗虽不读原表,但若分组数极大(如百万级用户ID),窗口排序仍可能成为瓶颈
最容易被忽略的陷阱:ORDER BY位置决定窗口内排序逻辑
很多人以为ORDER BY只控制最终结果顺序,其实它对窗口函数毫无影响——真正决定ROW_NUMBER()或AVG() OVER ()计算顺序的,是窗口函数自身的ORDER BY子句(写在OVER里面)。外层ORDER BY只排最终输出行,不改变窗口计算过程。
比如:SELECT user_id, order_date, amount, SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) FROM orders ORDER BY user_id,这里窗口内的累计和严格按order_date升序累加,而最终结果按user_id排列,两者完全独立。
- 混淆会导致“我以为按时间排序了,结果累计和乱序”——检查是否漏写了窗口函数内部的
ORDER BY - 如果窗口函数没写
ORDER BY(如COUNT(*) OVER (PARTITION BY dept)),则窗口内行序未定义,不同执行可能产生不同结果(尤其涉及LAG/LEAD时) - MySQL 5.7及更早版本不支持窗口函数,强行使用会报语法错误;8.0+才支持,且
ROWS帧语法需显式指定,RANGE默认行为在不同版本间有差异










