GROUP BY 分组
统计需求在业务里无处不在:"每个班多少人?""每个用户花了多少钱?""每月销售额多少?"——这些都是"按某字段分组,然后对每组算一个汇总值"。SQL 用 GROUP BY + 聚合函数搞定这套逻辑。
1. 五大聚合函数
聚合函数(Aggregate Function)把多行值聚合成一个值。最常用的有 5 个:
-- 聚合函数:把多行值聚合成一个值
-- 常用 5 个:COUNT、SUM、AVG、MAX、MIN
SELECT COUNT(*) FROM students; -- 总行数(学生总数)
SELECT COUNT(email) FROM students; -- email 非 NULL 的行数
SELECT COUNT(DISTINCT city) FROM students; -- 去重后不同城市的数量
SELECT AVG(age) FROM students; -- 平均年龄
SELECT SUM(salary) FROM employees; -- 工资总和
SELECT MAX(age), MIN(age) FROM students; -- 最大、最小年龄
-- ⚠️ COUNT(*) 和 COUNT(列) 的区别
-- COUNT(*) 统计【所有行】,包括 NULL
-- COUNT(列名) 统计【该列非 NULL】的行数
-- COUNT(DISTINCT 列) 统计该列不同值的数量COUNT(*) 和 COUNT(列) 的区别是面试必考题——COUNT(*) 统计所有行(含 NULL),COUNT(列) 只统计该列非 NULL 的行。如果要去重统计,用 COUNT(DISTINCT 列)。
2. GROUP BY 基本用法
光有聚合函数还不够——它对整张表算一个值。要按字段分组算,就要用 GROUP BY:
-- GROUP BY:按某字段分组,对每组算聚合值
-- 经典场景:统计每个班有多少学生
SELECT class_id, COUNT(*) AS student_count
FROM students
GROUP BY class_id;
-- 结果:
-- class_id | student_count
-- 1 | 25
-- 2 | 30
-- 3 | 22
-- 每一行的结果 = 一个分组
-- SELECT 里的列要么在 GROUP BY 里,要么被聚合函数包起来关键规则:SELECT 里的列,要么出现在 GROUP BY 里,要么被聚合函数包起来。否则 SQL 不知道取哪一行的值。MySQL 默认容忍这种写法(返回任意一行的值),但其他数据库严格报错。
3. 多列分组
GROUP BY 后面可以跟多个列,表示"先按第一列分组,组内再按第二列细分":
-- 多列分组:先按第一列,再按第二列
SELECT class_id, gender, COUNT(*) AS cnt
FROM students
GROUP BY class_id, gender;
-- 结果:
-- class_id | gender | cnt
-- 1 | 男 | 13
-- 1 | 女 | 12
-- 2 | 男 | 16
-- 2 | 女 | 14
-- 统计:每个班的男女分别多少人
-- 同时算多个指标
SELECT
class_id,
COUNT(*) AS student_count,
AVG(age) AS avg_age,
MAX(age) AS max_age,
MIN(age) AS min_age
FROM students
GROUP BY class_id;多列分组是统计多维数据的常用手段——按"班级 × 性别"统计人数、按"日期 × 渠道"统计订单、按"城市 × 商品类别"统计销量,套路都一样。
4. HAVING 分组后过滤
新手必懂的区分:WHERE 是"分组前"过滤行,HAVING 是"分组后"过滤组。两者作用时机不同,职能不可互换:
-- HAVING:对【分组后】的结果再加条件
-- 和 WHERE 的核心区别:
-- WHERE 分组【前】过滤行(不能用聚合函数)
-- HAVING 分组【后】过滤组(可以用聚合函数)
SELECT class_id, COUNT(*) AS cnt
FROM students
WHERE age >= 18 -- 先筛出成年学生(行级过滤)
GROUP BY class_id
HAVING COUNT(*) >= 20; -- 再筛出人数 ≥ 20 的班(组级过滤)
-- 执行流水线:
-- 1. FROM 取数据
-- 2. WHERE age >= 18 过滤掉未成年
-- 3. GROUP BY class_id 按班级分组
-- 4. 计算 COUNT(*)
-- 5. HAVING COUNT(*) >= 20 过滤人数不够的班
-- 6. SELECT 返回结果核心限制:WHERE 里不能用聚合函数(因为执行时还没分组,HAVING 可以)。理解了"先过滤行 → 再分组 → 再算聚合 → 再过滤组"这条流水线,GROUP BY 你就算通了。
5. 经典统计场景
下面三个是真实项目里高频出现的查询模式:
-- 经典统计场景
-- 1. 每个用户的订单数 + 总金额
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
ORDER BY total_spent DESC
LIMIT 100; -- Top 100 高消费用户
-- 2. 每天的销售额
SELECT
DATE(created_at) AS day,
COUNT(*) AS order_count,
SUM(amount) AS daily_revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY day DESC;
-- 3. 商品分类的销量排行
SELECT
category,
SUM(quantity) AS total_sold,
SUM(quantity * unit_price) AS revenue
FROM order_items
GROUP BY category
HAVING SUM(quantity) > 100
ORDER BY revenue DESC;这三个例子覆盖了"用户行为分析"、"日报表"、"商品排行榜"——所有复杂数据统计都是它们的组合。建议你把这三个 SQL 背下来,以后写报表查询时改改就行。
6. WITH ROLLUP 自动汇总
报表场景常需要在分组结果最后加一行"总计"。MySQL 提供 WITH ROLLUP 自动生成:
-- WITH ROLLUP:在分组结果最后加一行【总计】(MySQL 特有)
SELECT
class_id,
COUNT(*) AS student_count,
AVG(age) AS avg_age
FROM students
GROUP BY class_id WITH ROLLUP;
-- 结果:
-- class_id | student_count | avg_age
-- 1 | 25 | 20.5
-- 2 | 30 | 21.2
-- 3 | 22 | 20.8
-- NULL | 77 | 20.9 ← ROLLUP 行,所有班的总计
-- 多列 GROUP BY 时,ROLLUP 会生成多个层级的汇总
-- PostgreSQL 用 GROUPING SETS / ROLLUP() 函数
-- SQL 标准的 GROUP BY ROLLUP(a, b)多列 GROUP BY 时,ROLLUP 会生成多个层级的汇总(按主分组、按子分组、总计)。PostgreSQL 用 GROUP BY ROLLUP(a, b) 或 GROUPING SETS 实现相同效果。
7. GROUP BY 的常见坑
几个新手必踩的坑,提前看一遍能少走弯路:
-- ⚠️ GROUP BY 的常见坑
-- 1. SELECT 里的列必须在 GROUP BY 或聚合函数里(标准 SQL)
-- ❌ 错误(name 既不在 GROUP BY 也没聚合)
SELECT class_id, name FROM students GROUP BY class_id;
-- ✅ MySQL 默认容忍(返回任意一行的 name),但其他库报错
-- 严格模式 ONLY_FULL_GROUP_BY 下 MySQL 也报错
-- 2. WHERE 用聚合函数会报错
-- ❌ 错误
SELECT class_id FROM students WHERE COUNT(*) > 5 GROUP BY class_id;
-- ✅ 用 HAVING
SELECT class_id FROM students GROUP BY class_id HAVING COUNT(*) > 5;
-- 3. GROUP BY 字段顺序影响结果(多列分组时)
-- GROUP BY a, b 和 GROUP BY b, a 的分组逻辑不同
-- 4. NULL 也会被分到一个组
SELECT class_id, COUNT(*) FROM students GROUP BY class_id;
-- class_id 为 NULL 的行会单独成一组(class_id 列显示 NULL)其中第一条最容易出错——MySQL 默认模式容忍 SELECT 里出现非 GROUP BY 列,会让初学者以为这是合法语法。一旦切到 PostgreSQL/Oracle 或者开启 MySQL 严格模式,立刻报错。建议从一开始就按标准 SQL 写,养成好习惯。
窗口函数:GROUP BY 的升级版
GROUP BY 有个限制:分组后每组只返回一行,丢失了原始行。如果你既想看每行、又想看每组的聚合值(比如"每个学生 + 他所在班的平均分"),就要用窗口函数(MySQL 8.0+、PostgreSQL 都支持):
AVG(age) OVER (PARTITION BY class_id):每行附上所在班的平均分。ROW_NUMBER() OVER (ORDER BY score DESC):每行打一个全局排名。RANK() OVER (PARTITION BY class_id ORDER BY score DESC):每行打一个班内排名。
窗口函数是 SQL 的进阶利器,本教程不展开,但你必须知道它的存在——很多复杂的"Top N per group"问题,窗口函数一句话搞定,而 GROUP BY 要写子查询。
小结
GROUP BY + 聚合函数是 SQL 做统计的核心武器。记住"WHERE 过滤行 → GROUP BY 分组 → 聚合函数算值 → HAVING 过滤组"这条流水线,90% 的统计查询你都能解决。下一篇我们看子查询——把查询嵌套进查询,处理更复杂的逻辑。
← 上一篇 JOIN 连接
下一篇 子查询 →