因为group by会压缩行数,而排名必须保留在原始数据的每一行上;窗口函数在不改变结果集行数的前提下完成行级排序标记,这才是正确起点。

排名计算为什么不能用 GROUP BY?
因为 GROUP BY 会压缩行数,而排名必须保留在原始数据的每一行上。窗口函数在不改变结果集行数的前提下完成行级排序标记,这才是正确起点。
常见错误是写成 SELECT name, RANK() OVER (ORDER BY score) FROM scores GROUP BY name——这语法报错,且逻辑矛盾:GROUP BY 后 RANK() 失去上下文。
-
RANK():并列名次跳号(95、95、87 → 1、1、3) -
DENSE_RANK():并列不跳号(→ 1、1、2) -
ROW_NUMBER():强制唯一序号(→ 1、2、3),适合分页或取 Top-N - 多字段排序时,
ORDER BY score DESC, id ASC可避免相同分数下的随机波动
移动平均怎么避免“只算到当前行”这个坑?
默认 AVG() OVER (ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) 是累积平均,不是移动平均。真要算近 7 天均值,必须显式定义窗口帧。
错误写法:AVG(sales) OVER (ORDER BY date) —— 这实际等价于累积平均,首行就是自身,末行是全量均值。
- 正确写法:
AVG(sales) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) - 时间范围更安全?用
RANGE BETWEEN INTERVAL '6 days' PRECEDING AND CURRENT ROW,但注意:部分数据库(如 PostgreSQL)要求date列为TIMESTAMP类型,且索引支持有限 - 遇到缺失日期?先用
GENERATE_SERIES或左连接补全日期维度,否则窗口会“跳空”导致计算偏移
聚合类窗口函数和普通聚合混用的边界在哪?
可以共存,但必须分清层级:COUNT(*) OVER (PARTITION BY dept) 是每行都带部门总人数;AVG(salary) OVER () 是全表平均工资。两者能同时出现在同一 SELECT 中,但不能嵌套,比如 SUM(COUNT(*) OVER (...)) 是非法的。
- 想算“部门内薪资高于部门均值的人数”?先用
AVG(salary) OVER (PARTITION BY dept)算出部门均值列,再用条件聚合:COUNT(*) FILTER (WHERE salary > dept_avg)(PostgreSQL)或SUM(CASE WHEN salary > dept_avg THEN 1 ELSE 0 END) -
MIN()/MAX()窗口函数常被误用于“本组最大值对比”,但它不触发重排序,性能比子查询好得多 - 注意 NULL 处理:
COUNT(col) OVER (...)忽略 NULL,COUNT(*) OVER (...)统计所有行
MySQL 8.0+ 和旧版兼容性怎么破?
MySQL 在 8.0 才支持窗口函数,5.7 及之前版本执行含 OVER 的语句直接报错 ERROR 1064。没有降级方案,只能重构。
替代思路不是模拟窗口逻辑(那会极慢),而是评估是否真需要窗口语义:
- 简单排名?用自连接 + 计数:
SELECT a.name, COUNT(b.score >= a.score) AS rank FROM scores a LEFT JOIN scores b ON a.dept = b.dept GROUP BY a.name, a.score(仅适用于小表) - 移动平均?用相关子查询:
(SELECT AVG(sales) FROM sales s2 WHERE s2.date BETWEEN DATE_SUB(s1.date, INTERVAL 6 DAY) AND s1.date),但数据量过万就明显变慢 - 最稳妥方式:升级 MySQL 或前置用 Python/Pandas 做好计算再入库,别在 SQL 层硬扛
真正容易被忽略的是排序稳定性:没写 ORDER BY 子句的窗口函数,在不同执行计划下可能返回不一致的行序——哪怕只是加了个索引,ROW_NUMBER() 结果都可能变。生产环境务必显式声明 ORDER BY,哪怕多加一个 id 保底。










