做报表统计的时候,单层 GROUP BY 往往不够用。比如要先算出每个部门的平均工资,再从这些结果里挑出平均工资最高的三个部门,这就是典型的两层聚合。很多刚接触 SQL 的朋友会试图把 MAX 和 GROUP BY 直接塞进一条语句里,结果要么报错,要么统计出来的数据完全不对。这篇文章就来把嵌套聚合的原理和几种常见实现方式讲清楚。

先搞懂 SQL 的逻辑执行顺序
要理解嵌套聚合为什么必须拆开写,得先知道 SQL 语句各子句的执行顺序。虽然我们写 SQL 时习惯把 SELECT 放在最前面,但数据库引擎实际的执行顺序是 FROM、WHERE、GROUP BY、HAVING、SELECT、ORDER BY。也就是说,WHERE 在 GROUP BY 之前执行,此时数据还没被分组,聚合结果还不存在。
这就解释了一个经典报错:如果你写了 WHERE salary > AVG(salary),MySQL 或者 SQL Server 会直接告诉你聚合函数不允许出现在 WHERE 子句中。因为执行 WHERE 的时候,平均值根本还没算出来。想基于聚合结果做过滤,必须用 HAVING,或者把聚合放到子查询里再在外层筛选。
同样的道理,一层 GROUP BY 查完之后,结果集里的每一行代表一个分组,原始的明细行已经被压缩掉了。如果你还想在分组结果的基础上再做一次统计,比如对每个部门的平均工资再取最大值,就只能把第一次聚合的结果当作新的数据来源,再套一层查询。这就是嵌套聚合的本质:上一层的输出作为下一层的输入。
用子查询和派生表实现两层聚合
最直接的写法是把内层聚合包成一个派生表,外层再对它做统计。假设有一张员工表 emp,包含部门编号 dept_id 和工资 salary,现在要查出平均工资最高的前三个部门,可以这样写:
SELECT dept_id, avg_salary
FROM (
SELECT dept_id, AVG(salary) AS avg_salary
FROM emp
GROUP BY dept_id
) AS t
ORDER BY avg_salary DESC
LIMIT 3;内层查询先按部门分组算出平均工资,生成一个临时结果集 t,外层查询把这个结果集当成一张普通表来排序取前三。整个过程分两步走,逻辑非常清晰。这种写法在 MySQL、PostgreSQL、SQL Server 里都能跑,唯一需要注意的是 SQL Server 不支持 LIMIT,要换成 SELECT TOP 3 的写法。
如果只是想拿到一个聚合后的单值,比如所有部门平均工资里最高的那个平均值,还可以用标量子查询的思路:
SELECT MAX(avg_salary) AS max_avg
FROM (
SELECT dept_id, AVG(salary) AS avg_salary
FROM emp
GROUP BY dept_id
) AS t;派生表的方式虽然好理解,但层级多了以后可读性会下降。三层以上的聚合层层嵌套,SQL 会变得很难维护,这时候建议用 CTE(公用表表达式)来改造,每一层聚合起个名字,读起来就像读伪代码一样清晰。
窗口函数:不用嵌套也能解决大半问题
很多嵌套聚合的需求,其实用窗口函数可以一步到位。窗口函数的执行位置在 GROUP BY 之后、ORDER BY 之前,它可以在保留明细行的同时给出聚合结果,避免了反复嵌套子查询。比如要查出每个部门工资最高的员工姓名,传统写法需要一个子查询找出每个部门的最高工资,再回表匹配:
SELECT e.emp_name, e.dept_id, e.salary
FROM emp e
JOIN (
SELECT dept_id, MAX(salary) AS max_salary
FROM emp
GROUP BY dept_id
) AS m ON e.dept_id = m.dept_id AND e.salary = m.max_salary;用窗口函数则简洁得多:
SELECT emp_name, dept_id, salary
FROM (
SELECT emp_name, dept_id, salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk
FROM emp
) AS t
WHERE rk = 1;两种写法结果一致,但窗口函数版本扩展性更好:如果要求每个部门工资前三的员工,只要把条件改成 rk <= 3 就行,而 JOIN 写法要处理并列排名的问题,改动成本高得多。另外像累计求和、移动平均这类需要在聚合结果上再做计算的场景,窗口函数几乎是唯一优雅的解法。
当然窗口函数也有局限。它不能直接出现在 WHERE 里,所以还是要包一层子查询再筛选;而且在分组之后还要对聚合结果二次统计的场景,窗口函数和 GROUP BY 混用时要注意执行时机,必要时两者结合使用。
常见坑与性能建议
写嵌套聚合时最容易踩的坑有几个。第一是内层子查询忘了写别名,MySQL 会直接报错,虽然只是小问题但排查起来浪费不少时间。第二是分组字段在外层引用时写错名字,导致结果集为空却不报错,这种逻辑错误比语法错误更隐蔽。第三是在没有索引的大表上做多层聚合,每层都要扫全表,性能会急剧恶化。
从性能角度考虑,能过滤的尽量在内层提前过滤,让参与聚合的数据量最小化。比如只统计今年的数据,就在最内层的 WHERE 里加上时间条件,而不是等聚合完再筛。多层聚合时,如果内层结果集很小,派生表的开销可以忽略;如果内层结果集很大,考虑用临时表落盘或者物化视图来缓存中间结果。数据库优化器通常会合并简单的派生表,但包含聚合的子查询一般不会被合并,所以层级越深越要留意执行计划。
最后给一个三层聚合的综合例子收尾:统计每个销售区域每月的订单金额,再算每个区域的月均金额,最后取所有区域中月均金额最高的那个区域,用 CTE 写法如下:
WITH monthly AS (
SELECT region, DATE_FORMAT(order_time, '%Y-%m') AS mon,
SUM(amount) AS total
FROM orders
GROUP BY region, DATE_FORMAT(order_time, '%Y-%m')
),
region_avg AS (
SELECT region, AVG(total) AS avg_monthly
FROM monthly
GROUP BY region
)
SELECT region, avg_monthly
FROM region_avg
ORDER BY avg_monthly DESC
LIMIT 1;可以看到,只要理清每一层聚合的输入和输出,再复杂的统计需求拆开来写都不会太难。掌握派生表、CTE 和窗口函数这三种工具的适用边界,基本就能应付日常工作中绝大多数嵌套聚合场景了。
SQL嵌套聚合子查询聚合GROUP BY统计修改时间:2026-09-04 07:02:52