GROUP BY 本质是聚合,每组仅返回一行,无法保留明细;需用 ROW_NUMBER() 窗口函数定位每组 Top 1 明细,或用 AVG() OVER (PARTITION BY ...) 补全行级统计值。

为什么不能直接用 GROUP BY 获取明细行
GROUP BY 的本质是聚合,每组只返回一行结果,原始明细数据必然丢失。比如想查每个部门工资最高的员工姓名,GROUP BY dept 配合 MAX(salary) 只能拿到最高工资数值,拿不到对应的人名——因为 name 不在 GROUP BY 列里,也不满足函数依赖,多数数据库(如 PostgreSQL、MySQL 8.0+ 严格模式)会直接报错:ERROR: column "name" must appear in the GROUP BY clause or be used in an aggregate function。
用 ROW_NUMBER() + PARTITION BY 找每组 Top 1 明细
窗口函数不改变行数,能在保留每行原始字段的同时,按逻辑分组计算序号。关键不是“替代 GROUP BY”,而是绕过它实现聚合意图下的明细定位。
-
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)会给每个部门内员工按工资从高到低编号,第 1 名就是该部门最高薪者 - 外层再套一层查询过滤
rn = 1,就能拿到完整明细行(含name、salary、dept等所有原始列) - 注意:如果多个员工工资相同且并列第一,
ROW_NUMBER()会强制分出 1/2/3;要用并列不跳号,改用RANK()
SELECT name, salary, dept
FROM (
SELECT name, salary, dept,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) t
WHERE rn = 1;
用聚合窗口函数(SUM、AVG 等)直接补全统计值
当需要“每行都显示本部门平均工资”这类需求时,AVG(salary) OVER (PARTITION BY dept) 比先 GROUP BY 再 JOIN 回原表更简洁安全——没有关联键风险,也不会因多对一连接导致行数膨胀。
- 聚合窗口函数默认等价于
OVER (PARTITION BY ... RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING),即整组内计算 - 若误写成
ORDER BY而没指定ROWS或RANGE框架,PostgreSQL 会默认加ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,结果变成“累积平均”,不是你想要的组内均值 - MySQL 8.0+ 和 SQL Server 对未声明框架的行为更一致,但显式写全更可靠
SELECT name, salary, dept,
AVG(salary) OVER (PARTITION BY dept) AS dept_avg_salary,
COUNT(*) OVER (PARTITION BY dept) AS dept_emp_count
FROM employees;
容易忽略的 NULL 和排序稳定性问题
窗口函数中 ORDER BY 遇到 NULL 值,默认行为因数据库而异:PostgreSQL 默认 NULLS LAST,MySQL 8.0 默认 NULLS FIRST。如果工资字段有 NULL,又没显式声明,ROW_NUMBER() 排序结果可能跨库不一致。
- 明确写
ORDER BY salary DESC NULLS LAST消除歧义 - 即使所有值非空,也建议在
ORDER BY后加一个唯一列(如id)做二级排序,避免因数据插入顺序不同导致同一语句多次执行结果不一致 - SQL 标准不保证无
ORDER BY的窗口函数顺序,即使当前执行看起来稳定,也不能当作确定性行为依赖
真正难的不是写出语法,而是想清楚:你要的到底是“每组挑一行”,还是“每行带一组统计”,或是“动态滑动范围内的聚合”——选错窗口函数类型或漏掉框架定义,结果就 quietly 错了。










