聚合函数与分组GROUP BY

第三天我们学会了单表 CRUD,但现实中的需求往往不是”返回原始数据”,而是”算一下总数/平均值/最大值”。今天学的就是”怎么把数据分组后再算”。


一、分组聚合(GROUP BY)

1.1 为什么需要分组?

看这个需求:“每个用户分别花了多少钱?”

你可以写:

 
-- 一个一个查,查 8 个用户就要写 8 条 SQL
 
SELECT SUM(total) FROM orders WHERE user_id = 1;
 
SELECT SUM(total) FROM orders WHERE user_id = 2;
 
-- ... 写到手断
 

GROUP BY 就是”按某一列把数据分成几个组,然后对每组分别计算”。一句话搞定上面的事。

1.2 基本语法

 
SELECT 分组列, 聚合函数(计算列)
 
FROM 表名
 
GROUP BY 分组列;
 
 
-- 每个用户的消费总额
 
SELECT user_id, SUM(total) AS total_spent
 
FROM orders
 
GROUP BY user_id;
 

发生了什么?


graph LR

    subgraph raw["原始数据(orders 表)"]

        r1["user_id=1, total=299"]

        r2["user_id=1, total=298"]

        r3["user_id=1, total=1999"]

        r4["user_id=2, total=145"]

        r5["user_id=3, total=259"]

        r6["user_id=3, total=59.70"]

        r7["user_id=3, total=2499"]

    end

    subgraph grouped["GROUP BY user_id 后"]

        g1["user_id=1 → SUM=2596.00"]

        g2["user_id=2 → SUM=145.00"]

        g3["user_id=3 → SUM=2817.70"]

    end

    r1 --> g1

    r2 --> g1

    r3 --> g1

    r4 --> g2

    r5 --> g3

    r6 --> g3

    r7 --> g3

GROUP BY 把相同的 user_id 挤到一起,然后 SUM 对每组内的 total 求和。

1.3 常用聚合函数

函数作用例子
COUNT(*)统计行数每个用户有多少笔订单
SUM(列)求和每个用户的总消费
AVG(列)求平均值每个用户的平均订单金额
MAX(列)取最大值每个用户最贵的一笔订单
MIN(列)取最小值每个用户最便宜的一笔订单
 
-- 全部一起上
 
SELECT
 
    user_id,
 
    COUNT(*)    AS order_count,     -- 订单数
 
    SUM(total)  AS total_spent,     -- 总消费
 
    AVG(total)  AS avg_order,       -- 平均每单金额
 
    MAX(total)  AS max_order,       -- 最贵一单
 
    MIN(total)  AS min_order        -- 最便宜一单
 
FROM orders
 
GROUP BY user_id;
 

输出:

 
+---------+-------------+-------------+-----------+-----------+-----------+
 
| user_id | order_count | total_spent | avg_order | max_order | min_order |
 
+---------+-------------+-------------+-----------+-----------+-----------+
 
|       1 |           3 |     2596.00 |  865.3333 |   1999.00 |    298.00 |
 
|       2 |           1 |      145.00 |  145.0000 |    145.00 |    145.00 |
 
|       3 |           3 |     2817.70 |  939.2333 |   2499.00 |     59.70 |
 
|       4 |           1 |      149.00 |  149.0000 |    149.00 |    149.00 |
 
|       5 |           3 |     2702.00 |  900.6667 |   1999.00 |    105.00 |
 
|       6 |           1 |       19.90 |   19.9000 |     19.90 |     19.90 |
 
|       8 |           1 |      299.00 |  299.0000 |    299.00 |    299.00 |
 
+---------+-------------+-------------+-----------+-----------+-----------+
 

1.4 GROUP BY 的铁律

SELECT 里的列,要么出现在 GROUP BY 中,要么被聚合函数包裹。否则报错或拿到随机值。

 
-- ❌ 错误:product 既不在 GROUP BY 里,也没被聚合函数包裹
 
SELECT user_id, product, SUM(total)
 
FROM orders
 
GROUP BY user_id;
 
-- MySQL 可能不报错但 product 的值是随机的,毫无意义
 
-- ✅ 正确
 
SELECT user_id, SUM(total)
 
FROM orders
 
GROUP BY user_id;
 

IMPORTANT

原因: 分组后,user_id=1 对应了 3 行(键盘、鼠标、显示器),聚合函数知道怎么处理这 3 行的 total(求和),但 product 这 3 行分别是不同的值——该显示哪个?数据库没法替你做决定。

1.5 多字段分组

 
-- 每个用户买了每种商品的总数量
 
SELECT user_id, product, SUM(quantity) AS total_qty
 
FROM orders
 
GROUP BY user_id, product
 
ORDER BY user_id, total_qty DESC;
 

输出:

 
+---------+--------------+-----------+
 
| user_id | product      | total_qty |
 
+---------+--------------+-----------+
 
|       1 | 鼠标         |         2 |
 
|       1 | 机械键盘     |         1 |
 
|       1 | 显示器       |         1 |
 
|       2 | 鼠标垫       |         5 |
 
|       3 | 数据线       |         3 |
 
|       3 | 机械键盘     |         1 |
 
|       3 | 显示器       |         1 |
 
|       ...| ...          |       ... |
 
+---------+--------------+-----------+
 

多字段分组:先按 user_id 分,每组内再按 product 分。等价于”每个用户的每种商品”。


二、分组后筛选(HAVING)

2.1 为什么 WHERE 不够用?

 
-- 我想查"消费总额超过 500 的用户"
 
-- ❌ 不能用 WHERE:WHERE 执行时还没有 SUM(total) 这个计算结果
 
SELECT user_id, SUM(total) AS total_spent
 
FROM orders
 
WHERE SUM(total) > 500   -- 报错!WHERE 里不能用聚合函数
 
GROUP BY user_id;
 

WHERE 是分组前筛行,HAVING 是分组后筛组。 完整执行顺序:

 
① FROM orders          —— 拿出订单表
 
② WHERE 条件           —— 筛掉不满足条件的行(此时还没有分组)
 
③ GROUP BY user_id     —— 分组
 
④ 聚合函数计算          —— SUM、COUNT、AVG...
 
⑤ HAVING 条件          —— 筛掉不满足条件的组(此时聚合结果已经有了)
 
⑥ SELECT               —— 挑出要显示的列
 
⑦ ORDER BY             —— 排序
 
⑧ LIMIT                —— 分页
 

2.2 HAVING 基本用法

 
-- 查询消费总额超过 500 的用户
 
SELECT user_id, SUM(total) AS total_spent
 
FROM orders
 
GROUP BY user_id
 
HAVING SUM(total) > 500;
 

输出:

 
+---------+-------------+
 
| user_id | total_spent |
 
+---------+-------------+
 
|       1 |     2596.00 |
 
|       3 |     2817.70 |
 
|       5 |     2702.00 |
 
+---------+-------------+
 

2.3 WHERE 和 HAVING 同时用

 
-- 查询"张三买的商品中,每种商品买了超过 1 件的"
 
SELECT user_id, product, SUM(quantity) AS total_qty
 
FROM orders
 
WHERE user_id = 1              -- ① 先筛:只要张三的订单
 
GROUP BY user_id, product       -- ② 再分组
 
HAVING SUM(quantity) > 1;       -- ③ 再筛:只要数量 >1 的组
 

输出:

 
+---------+---------+-----------+
 
| user_id | product | total_qty |
 
+---------+---------+-----------+
 
|       1 | 鼠标    |         2 |
 
+---------+---------+-----------+
 

2.4 HAVING vs WHERE 对比

WHEREHAVING
执行时机分组前(筛行)分组后(筛组)
能用聚合函数吗❌ 不能✅ 可以
能用列名吗✅ 可以✅ 可以(但只能是分组列)
性能快(先筛掉,分组数据量小)慢(分组完再筛,已经算完了)
最佳实践能放 WHERE 的就别放 HAVING只有涉及聚合函数才用 HAVING

IMPORTANT

原则:行级筛选用 WHERE,聚合结果筛选用 HAVING。 不要把所有条件都堆在 HAVING 里——先 WHERE 筛掉不相关的行,分组的数据量更小,查询更快。


速查表

操作语法一句话解释
分组GROUP BY 列按某列的值分成几个组
分组后筛选HAVING 聚合条件分组算完再筛(如总消费 > 500)
COUNTCOUNT(*) / COUNT(列)统计行数(列 NULL 的不算)
SUMSUM(列)求和
AVGAVG(列)求平均
MAX/MINMAX(列) / MIN(列)最大值 / 最小值
多字段分组GROUP BY 列1, 列2先按列1分,每组内再按列2分
去重计数COUNT(DISTINCT 列)统计不重复值的个数
COALESCECOALESCE(列, 默认值)如果列是 NULL,返回默认值
铁律SELECT 非聚合列必须出现在 GROUP BY否则数据随机

速记卡(面试闪卡)

Q1:一句话讲清「聚合函数与分组GROUP BY」到底是什么?

A:GROUP BY 按某列把数据分成几组,再对每组用 COUNT/SUM/AVG 等聚合函数算出一条汇总。

Q2:一、分组聚合(GROUP BY) —— 怎么理解?

A:GROUP BY 像按用户分拣订单:相同 user_id 的行挤成一叠,每叠用 SUM 算总花费,一个 SQL 顶 N 条(aggregation 聚合)。

Q3:1.3 常用聚合函数 —— 怎么理解?

A:五个聚合兄弟最常用:COUNT 数行、SUM 求和、AVG 平均、MAX 最大、MIN 最小,一次全上每组算一个数(aggregate function 聚合函数)。

Q4:1.4 GROUP BY 的铁律 —— 怎么理解?

A:铁律像”座位规则”:SELECT 里的列要么进 GROUP BY,要么被聚合函数包住,否则返回随机值(grouping column 分组列)。

Q5:二、分组后筛选(HAVING) —— 怎么理解?

A:HAVING 像分组后的门卫:WHERE 管行(分组前、不能用聚合)、HAVING 管组(分组后、能用 SUM)(HAVING clause 筛选子句)。

Q6:核心速记主线有哪些?

  • GROUP BY 按列分组,同组行聚合出一条汇总

  • 五函数:COUNT/SUM/AVG/MAX/MIN 每组算一个数

  • 铁律:SELECT 非聚合列必须进 GROUP BY,否则随机值

  • WHERE 管行、HAVING 管组;聚合结果筛选只能放 HAVING

口诀

A:GROUP BY 分组算,聚合五兄弟 COUNT SUM AVG MAX MIN;

SELECT 列两归宿:进分组或被聚合,否则随机值;

WHERE 管行 HAVING 管组,聚合结果 HAVING 滤;

一个 SQL 顶 N 条,汇总统计不发愁。

相关链接


技术学习路线图 > 五、MySQL 实战