窗口函数不压缩行数,输出行数严格等于输入行数;聚合函数经group by后行数减少。二者在null处理、执行逻辑、性能及语法细节上存在本质差异。

窗口函数不压缩行数,聚合函数天然分组折叠
最直接的区别是输出行数:执行 COUNT(*) 或 SUM(salary) 时,只要用了 GROUP BY,结果必然少于或等于原表行数;而 COUNT(*) OVER () 或 SUM(salary) OVER (PARTITION BY dept) 的输出行数严格等于输入行数——每行都带一个计算值,原始字段全保留。
这意味着你不能靠“有没有 GROUP BY”来判断是否丢失明细。常见误判场景:
- 想查每个员工工资 + 所在部门平均工资,却写了
SELECT name, salary, AVG(salary) FROM emp GROUP BY dept→ 报错或结果错乱,因为name不在GROUP BY里且没被聚合 - 正确写法是去掉
GROUP BY,改用AVG(salary) OVER (PARTITION BY dept) - 聚合函数默认跳过
NULL,但窗口函数在ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW这类帧中,NULL仍占位置——帧长度固定,有效参与计算的行数却可能变少
OVER 子句定义作用域,不是简单“加个括号”
OVER 不是语法装饰,它显式声明三件事:分区(PARTITION BY)、排序(ORDER BY)、帧范围(ROWS / RANGE)。缺一不可,且顺序和组合影响结果。
例如 SUM(sales) OVER (PARTITION BY region ORDER BY date) 和 SUM(sales) OVER (PARTITION BY region ORDER BY date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 看似一样,实则后者明确限定为“从分区首行到当前行”,前者在某些数据库(如旧版 MySQL)中可能默认用 RANGE,遇到相同 date 值会把同天所有行都纳入计算——导致累计和突增。
关键点:
-
PARTITION BY决定“和谁比”,不写等价于全表一个分区 -
ORDER BY在排名/累计类函数中强制要求;在SUM/AVG中若没配帧,部分数据库会报错 -
ROWS按物理行偏移,RANGE按排序值逻辑区间,二者在重复排序键下行为差异极大
同一函数名,不同上下文语义完全不同
SUM()、AVG()、COUNT() 这些名字在聚合和窗口中写法一样,但执行逻辑割裂:
-
COUNT(col) WHERE status = 'active'是条件计数,返回单值 -
COUNT(col) OVER (PARTITION BY team)是按团队分别计非空col值,每行返回对应团队的总数 -
ROW_NUMBER() OVER (ORDER BY score DESC)没有聚合语义,纯排序编号,不能套SUM(ROW_NUMBER()) -
PERCENT_RANK() OVER (PARTITION BY dept ORDER BY salary)依赖排序位置算相对排名,和任何聚合无关
最容易踩的坑是把窗口函数当“增强版聚合”去理解——它本质是逐行计算,只是复用了聚合函数的名称和部分逻辑。
性能差异来自执行计划层级
聚合函数通常触发哈希分组或流式分组,数据扫描一次、分组一次、输出一次;窗口函数需要先排序(除非已有合适索引),再按帧滑动计算,内存开销更高,尤其在 ROWS BETWEEN ... PRECEDING AND FOLLOWING 这类双向帧中。
实际优化建议:
- 确保
OVER中的PARTITION BY和ORDER BY列有联合索引,否则排序成本陡增 - 避免在大表上用
RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING,等价于全分区扫描 -
LAG()/LEAD()比等价的自连接快得多,但必须有确定排序,否则结果不可靠 - MySQL 8.0+、PostgreSQL、SQL Server 支持窗口函数,但 SQLite 目前不支持(截至 2026 年 4 月)
真正难处理的不是语法,而是帧边界与 NULL 共存时的隐式行为——比如 AVG(val) OVER (ORDER BY ts ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) 遇到中间 3 个 NULL,均值分母是 4 而不是 7,但窗口长度仍是 7 行。这点几乎没人会在测试数据里刻意覆盖。











