📌 一句话总结
GROUP BY 将数据按列分组,配合聚合函数做组内统计;HAVING 对分组后的结果做筛选,是 WHERE 的"聚合后版本"。
1. GROUP BY 基础
sql
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
执行顺序:
FROM→ 取表WHERE→ 过滤行GROUP BY→ 分组- 聚合函数 → 每组计算
SELECT→ 选列ORDER BY→ 排序
sql
-- 多列分组
SELECT department_id, gender, COUNT(*) AS count
FROM employees
GROUP BY department_id, gender;
-- 每个部门+性别组合的行数
2. WHERE vs HAVING --- 执行时机完全不同
sql
-- WHERE:聚合前过滤(筛行)
-- HAVING:聚合后过滤(筛组)
-- 正确写法:先 WHERE 过滤掉离职员工,再按部门聚合
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE status = '在职' -- 聚合前过滤
GROUP BY department_id
HAVING AVG(salary) > 10000; -- 聚合后筛组
-- ❌ 错误:WHERE 不能引用聚合结果
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE AVG(salary) > 10000 -- 聚合还没执行,AVG 不能用
GROUP BY department_id;
| 对比 | WHERE | HAVING |
|---|---|---|
| 执行时机 | GROUP BY 之前 |
GROUP BY 之后 |
| 能否用聚合函数 | ❌ 不能 | ✅ 可以 |
| 能否用别名 | ❌ 不能(SELECT 还没执行) | ❌ 不能(逻辑顺序上) |
| 适用范围 | 行级别过滤 | 组级别过滤 |
⚠️
HAVING中也不能用SELECT中的别名,因为 SQL 逻辑执行顺序中HAVING在SELECT之前。MySQL 和 SQLite 宽松允许,但 PG 严格报错。
sql
-- MySQL/SQLite 宽松写法(不推荐)
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING avg_salary > 10000; -- 可以用别名
-- 标准写法(推荐,所有数据库通用)
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 10000; -- 重复写聚合表达式
3. 多列分组实战
sql
-- 按年份+部门统计
SELECT
EXTRACT(YEAR FROM hire_date) AS hire_year,
department_id,
COUNT(*) AS hired_count,
ROUND(AVG(salary), 0) AS avg_salary
FROM employees
GROUP BY EXTRACT(YEAR FROM hire_date), department_id
ORDER BY hire_year DESC, department_id;
-- 按城市+职位统计
SELECT
city,
job_title,
COUNT(*) AS employee_count,
ROUND(AVG(salary), 0) AS avg_salary
FROM employees
GROUP BY city, job_title
HAVING COUNT(*) >= 5 -- 只显示人数 >= 5 的组
ORDER BY avg_salary DESC;
4. 常见报错与陷阱
陷阱 1:SELECT 的列没在 GROUP BY 里
sql
-- ❌ 错误
SELECT name, department_id, AVG(salary)
FROM employees
GROUP BY department_id;
-- name 不在 GROUP BY 中,PG 直接报错,MySQL 随便取一个
-- ✅ 正确
SELECT department_id, AVG(salary)
FROM employees
GROUP BY department_id;
陷阱 2:WHERE 过滤聚合结果
sql
-- ❌ 错误:想找部门平均薪资 > 10000
SELECT department_id, AVG(salary)
FROM employees
WHERE AVG(salary) > 10000
GROUP BY department_id;
-- ✅ 正确:用 HAVING
SELECT department_id, AVG(salary)
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 10000;
陷阱 3:HAVING + WHERE 混用顺序
sql
-- WHERE 先过滤低薪员工,再分组,再 HAVING 筛组
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
WHERE salary > 5000 -- 1. 排除低薪
GROUP BY department_id -- 2. 分组
HAVING AVG(salary) > 15000 -- 3. 只看高薪组
ORDER BY avg_salary DESC; -- 4. 排序
5. 实战案例
案例 1:部门人才分析
sql
SELECT
department_id,
COUNT(*) AS 人数,
ROUND(AVG(salary), 0) AS 平均薪资,
MAX(salary) AS 最高薪资,
MIN(salary) AS 最低薪资,
ROUND(AVG(DATEDIFF(CURDATE(), hire_date)/365), 1) AS 平均司龄_年
FROM employees
WHERE status = '在职'
GROUP BY department_id
HAVING 人数 >= 3
ORDER BY 平均薪资 DESC;
案例 2:订单分析
sql
-- 每个客户的消费统计
SELECT
customer_id,
COUNT(*) AS order_count,
SUM(total_amount) AS total_spent,
AVG(total_amount) AS avg_order,
MAX(order_date) AS last_order_date
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY customer_id
HAVING order_count >= 5 -- 回头客
AND total_spent > 10000 -- 高价值客户
ORDER BY total_spent DESC;
案例 3:ROLLUP 分组汇总(进阶)
sql
-- MySQL/PostgreSQL: GROUP BY ROLLUP 自动加小计和总计
SELECT
COALESCE(department_id, '所有部门') AS department,
COALESCE(gender, '合计') AS gender,
COUNT(*) AS count,
AVG(salary) AS avg_salary
FROM employees
GROUP BY ROLLUP (department_id, gender);
-- 结果会多出行:部门小计行(gender=合计)+ 总计行(所有部门)
6. GROUP BY 的执行顺序细节
完整 SQL 逻辑顺序:
sql
SELECT -- 5
department_id,
AVG(salary) AS avg_salary
FROM employees -- 1
WHERE salary > 0 -- 2
GROUP BY dept_id -- 3
HAVING AVG(salary) > 10000 -- 4
ORDER BY avg_salary DESC -- 6
LIMIT 5; -- 7
这个顺序决定了为什么
WHERE不能用聚合函数、为什么HAVING通常不能用别名。
📝 要点总结
| 概念 | 关键点 |
|---|---|
GROUP BY |
按列分组,每组一行结果 |
WHERE |
聚合前过滤行 |
HAVING |
聚合后过滤组,可以用聚合函数 |
| SELECT 非聚合列 | 必须出现在 GROUP BY 中 |
| 执行顺序 | WHERE → GROUP BY → HAVING → SELECT → ORDER BY |
| ROLLUP | 自动添加小计/总计行 |
| NULL 分组 | NULL 也会自成一组 |