group by压缩行数而窗口函数保留原表结构:前者结果行数≤原表,后者恒等于原表行数;group by需聚合非分组字段,窗口函数可直接保留明细;窗口函数必须显式指定partition by和order by,且执行晚于where,须用子查询引用。

GROUP BY 压缩行数,窗口函数保留原表结构
这是最根本、也最容易被忽略的差异:GROUP BY 后结果行数 ≤ 原表行数,窗口函数输出行数 = 原表行数。
比如一张含 10 万条订单的表,GROUP BY customer_id 可能只返回几千行(每个客户一行);而 SUM(amount) OVER (PARTITION BY customer_id) 仍返回 10 万行,每行多一列“该客户总金额”。
常见错误现象:SELECT name, SUM(amount) FROM orders GROUP BY customer_id 在 MySQL 8.0 严格模式下直接报错,因为 name 既没在 GROUP BY 里,也没被聚合;但 SELECT name, SUM(amount) OVER (PARTITION BY customer_id) 完全合法,name 原样保留。
窗口函数必须显式定义 PARTITION BY 和 ORDER BY,GROUP BY 不需要
窗口函数不自动继承 GROUP BY 的分组逻辑,也不默认按任何字段排序——缺 PARTITION BY 就是全表一个窗口,缺 ORDER BY 则排名类函数(如 ROW_NUMBER())行为不可靠,累计类函数(如 SUM() OVER ())可能因数据库实现不同而结果不一致。
实操建议:
-
PARTITION BY决定“和谁比”,不写等价于PARTITION BY 1(全表一区) -
ORDER BY在ROW_NUMBER()、RANK()、累计求和中是强制要求,否则多数数据库会报错 - 帧子句(如
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)不是可选装饰,它明确控制计算范围,不写可能触发默认RANGE行为,在重复排序值时导致意外交集
WHERE 里不能用窗口函数,但可以嵌套子查询调用
WHERE ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) = 1 这种写法必定报错:ERROR 3593: Window function 'row_number' is not allowed in this context,因为窗口函数执行时机晚于 WHERE。
正确做法只能是两层结构:
SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) AS rn FROM events ) t WHERE rn = 1
容易踩的坑:
- 把
ORDER BY写在外层查询,会导致内层窗口排序失效,rn分配错乱 - 子查询里写
SELECT *会拖慢性能,尤其宽表场景,应只选必要字段 - MySQL 8.0+ 支持 CTE,但 CTE 本身不改变执行顺序,
WHERE仍不能直接引用窗口别名
GROUP BY 和窗口函数能混用,但顺序不可逆
SQL 执行顺序决定了:GROUP BY 先发生,窗口函数后计算。这意味着你可以在聚合结果上再开窗,但不能对窗口结果做 GROUP BY(除非再套一层)。
合法示例:
SELECT dept, SUM(salary) AS dept_total,
AVG(SUM(salary)) OVER (PARTITION BY region) AS region_avg
FROM staff GROUP BY dept, region
这里先按 dept 和 region 分组求和,再对分组后的结果按 region 算平均——AVG(SUM(salary)) 是允许的。
反向操作不行:SELECT *, ROW_NUMBER() OVER (PARTITION BY dept) FROM (...) GROUP BY dept 语法直接失败,因为窗口函数还没算,GROUP BY 就要压缩行了。
真正复杂的地方在于:多个窗口函数若分区或排序不同,可能触发多次数据扫描——尤其在 PostgreSQL 或 SQL Server 中,引擎未必能合并物理执行计划。MySQL 8.0 对单表多窗口有优化,但 JOIN 后的窗口仍易退化。











