分组查询
分组查询
🌍 生活类比
想象你是一个班主任,手里有一沓成绩单。你想知道:
- 每个学生的平均分 → 按学生分组,算平均
- 每科的最高分 → 按科目分组,找最大值
- 每个班的及格率 → 按班级分组,算比例
这就是 GROUP BY 的作用:先把数据分成若干小组,再对每个小组进行统计。上一课学的聚合函数是对整张表算一个值,GROUP BY 则是按类别分别算值。
🎯 核心概念
GROUP BY 基本语法
SELECT 列名, 聚合函数(列名)
FROM 表名
WHERE 条件
GROUP BY 列名;
执行逻辑:先按 GROUP BY 的列分组,再对每组执行聚合函数。
-- 每个部门有多少人
SELECT department_id, COUNT(*) AS 人数
FROM employees
GROUP BY department_id;
输出:
department_id 人数
------------- ----
1 3
2 2
3 2
NULL 1
SELECT 中的列,要么出现在 GROUP BY 中,要么用聚合函数包裹。否则语义不明确,数据库会报错。
分组后聚合
GROUP BY 的核心模式是:分组 → 聚合。
-- 每个部门的平均工资
SELECT department_id, AVG(salary) AS 平均工资
FROM employees
GROUP BY department_id;
常用的聚合组合:
SELECT department_id,
COUNT(*) AS 人数,
SUM(salary) AS 工资总额,
AVG(salary) AS 平均工资,
MAX(salary) AS 最高工资,
MIN(salary) AS 最低工资
FROM employees
GROUP BY department_id;
多列分组
可以按多个列分组,只有当这些列的值同时相同时才算同一组:
-- 每个部门、每种状态的员工人数
SELECT department_id, status, COUNT(*) AS 人数
FROM employees
GROUP BY department_id, status;
输出:
department_id status 人数
------------- -------- ----
1 active 3
2 active 1
2 inactive 1
3 active 2
GROUP BY + ORDER BY
分组后可以排序:
-- 按部门统计平均工资,从高到低排序
SELECT department_id, AVG(salary) AS 平均工资
FROM employees
GROUP BY department_id
ORDER BY 平均工资 DESC;
WHERE vs HAVING — 过滤行 vs 过滤组
这是本课最重要的概念之一:
| 特性 | WHERE | HAVING |
|---|---|---|
| 过滤对象 | 行(分组前) | 组(分组后) |
| 执行时机 | 在 GROUP BY 之前 |
在 GROUP BY 之后 |
| 能否用聚合函数 | ❌ 不能 | ✅ 可以 |
| 配合使用 | 可以单独用 | 必须配合 GROUP BY |
-- WHERE:先过滤行,再分组
-- 找出有3人以上的部门
SELECT department_id, COUNT(*) AS 人数
FROM employees
WHERE salary > 5000 -- 先排除工资低于5000的行
GROUP BY department_id;
-- HAVING:先分组,再过滤组
-- 找出人数>=3的部门
SELECT department_id, COUNT(*) AS 人数
FROM employees
GROUP BY department_id
HAVING COUNT(*) >= 3; -- 过滤人数不足3的组
-- WHERE + HAVING 组合使用
-- 找出2024年入职、且平均工资>10000的部门
SELECT department_id, AVG(salary) AS 平均工资
FROM employees
WHERE hire_date >= '2024-01-01' -- 先过滤行
GROUP BY department_id
HAVING AVG(salary) > 10000; -- 再过滤组
执行顺序
SQL查询的书写顺序和执行顺序不同:
书写顺序:SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY
执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
理解执行顺序对写出正确的SQL至关重要:
FROM— 确定数据源WHERE— 逐行过滤GROUP BY— 分组HAVING— 过滤分组SELECT— 选择列、计算表达式ORDER BY— 排序
WHERE 中不能用聚合函数——因为 WHERE 执行时,GROUP BY 还没执行,聚合结果还不存在。
📝 基本语法
-- 基本分组
SELECT 列名, 聚合函数(列名)
FROM 表名
GROUP BY 列名;
-- 多列分组
SELECT 列名1, 列名2, 聚合函数(列名)
FROM 表名
GROUP BY 列名1, 列名2;
-- WHERE + GROUP BY + HAVING
SELECT 列名, 聚合函数(列名)
FROM 表名
WHERE 行级条件
GROUP BY 列名
HAVING 聚合级条件
ORDER BY 排序列;
GROUP BY中的列不需要和SELECT中的列顺序一致,但必须包含所有非聚合列HAVING中可以使用聚合函数的别名(MySQL、PostgreSQL支持),但标准SQL要求写完整表达式NULL值在GROUP BY中会被归为一组
📌 示例
示例:按部门统计员工信息
SELECT d.department_name,
COUNT(e.employee_id) AS 人数,
AVG(e.salary) AS 平均工资,
SUM(e.salary) AS 工资总额
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id
GROUP BY d.department_name
ORDER BY 工资总额 DESC;
输出:
department_name 人数 平均工资 工资总额
--------------- ---- --------- ----------
技术部 3 17666.67 53000.00
财务部 2 13500.00 27000.00
市场部 2 11500.00 23000.00
行政部 1 9000.00 9000.00
LEFT JOIN 确保没有员工的部门也会显示(人数为0)。GROUP BY department_name 按部门名称分组。
示例:WHERE + GROUP BY + HAVING 组合
-- 2024年入职的员工中,找出平均工资>12000的部门
SELECT department_id,
COUNT(*) AS 人数,
AVG(salary) AS 平均工资
FROM employees
WHERE hire_date >= '2024-01-01'
GROUP BY department_id
HAVING AVG(salary) > 12000
ORDER BY 平均工资 DESC;
输出:
department_id 人数 平均工资
------------- ---- ---------
1 2 18500.00
3 1 14000.00
WHERE hire_date >= '2024-01-01'— 先筛出2024年入职的员工GROUP BY department_id— 按部门分组HAVING AVG(salary) > 12000— 只保留平均工资>12000的组ORDER BY 平均工资 DESC— 按平均工资降序排列
示例:多列分组 + 复杂条件
-- 每个部门、每种状态的员工统计,只看人数>=2的组
SELECT d.department_name,
e.status,
COUNT(*) AS 人数,
MIN(e.salary) AS 最低工资,
MAX(e.salary) AS 最高工资
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE e.salary > 0
GROUP BY d.department_name, e.status
HAVING COUNT(*) >= 2
ORDER BY d.department_name, 人数 DESC;
输出:
department_name status 人数 最低工资 最高工资
--------------- ------ ---- --------- ----------
技术部 active 3 15000.00 20000.00
财务部 active 2 12000.00 14000.00
市场部 active 2 10000.00 12000.00
WHERE 排除工资为0的行,再按部门+状态分组,HAVING 过滤掉人数不足2的组,最后排序。
🎬 场景演练
场景一:销售报表——按月统计订单
生成每月的销售报表,只显示订单数>=3的月份。
SELECT
EXTRACT(YEAR FROM order_date) AS 年份,
EXTRACT(MONTH FROM order_date) AS 月份,
COUNT(*) AS 订单数,
SUM(total_amount) AS 总金额,
AVG(total_amount) AS 平均订单金额
FROM orders
WHERE status IN ('completed', 'shipped')
GROUP BY EXTRACT(YEAR FROM order_date), EXTRACT(MONTH FROM order_date)
HAVING COUNT(*) >= 3
ORDER BY 年份, 月份;
WHERE 过滤出已完成和已发货的订单,按年月分组统计,HAVING 过滤掉订单数不足3的月份。
场景二:HR分析——找出高薪部门
找出平均工资高于公司整体平均工资的部门。
SELECT d.department_name,
COUNT(e.employee_id) AS 人数,
AVG(e.salary) AS 部门平均工资
FROM departments d
JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_name
HAVING AVG(e.salary) > (SELECT AVG(salary) FROM employees)
ORDER BY 部门平均工资 DESC;
HAVING 中可以使用子查询。先计算每个部门的平均工资,再与公司整体平均工资比较。这是聚合函数与子查询的结合应用。
❓ 常见问题
Q:SELECT中的列必须出现在GROUP BY中吗? A: 是的,所有非聚合列都必须出现在
GROUP BY中。这是SQL标准的要求。例如SELECT a, b, COUNT(*) FROM t GROUP BY a会报错,因为b既不在GROUP BY中,也没有被聚合函数包裹。
Q:HAVING能用SELECT中的别名吗? A: MySQL和PostgreSQL支持在
HAVING中使用别名,但SQL Server不支持。为了兼容性,建议写完整表达式:HAVING AVG(salary) > 10000而不是HAVING 平均工资 > 10000。
Q:GROUP BY中的NULL值怎么处理? A:
NULL在分组时会被归为一组。如果department_id有NULL值,所有NULL的行会被分到同一组。这通常是有用的,但要注意区分"没有部门"和"部门ID为0"。
Q:WHERE和HAVING可以同时用吗? A: 可以,而且很常见。
WHERE在分组前过滤行,HAVING在分组后过滤组。执行顺序:WHERE→GROUP BY→HAVING。
📖 小节
GROUP BY将数据分成若干组,对每组分别执行聚合函数SELECT中的非聚合列必须出现在GROUP BY中- 多列分组:按多个列的组合值分组
WHERE:分组前过滤行,不能用聚合函数HAVING:分组后过滤组,可以用聚合函数- 执行顺序:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY NULL在分组中被归为一组
📝 作业
作业1(⭐):查询每个部门的员工人数和平均工资,按平均工资从高到低排序。
作业2(⭐⭐):查询每个部门中2024年以后入职的员工人数,只显示人数>=2的部门。
作业3(⭐⭐⭐):查询每个部门的最高工资员工。要求显示部门名称、员工姓名和工资。提示:可以用子查询或 ROW_NUMBER() 窗口函数(后续学习)实现。
下一课
👉 15-advanced-functions - 高级函数:学习SQL高级函数,掌握字符串处理、数值计算、日期操作等实用技能!



