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 都支持):

窗口函数是 SQL 的进阶利器,本教程不展开,但你必须知道它的存在——很多复杂的"Top N per group"问题,窗口函数一句话搞定,而 GROUP BY 要写子查询。

小结

GROUP BY + 聚合函数是 SQL 做统计的核心武器。记住"WHERE 过滤行 → GROUP BY 分组 → 聚合函数算值 → HAVING 过滤组"这条流水线,90% 的统计查询你都能解决。下一篇我们看子查询——把查询嵌套进查询,处理更复杂的逻辑。

← 上一篇 JOIN 连接

下一篇 子查询

✈️💬