聚合函数与分组统计
本教程共 46 篇 · 第 23 篇 · 更新于 2026-07-30 · 约 11 分钟阅读
23. 聚合函数与分组统计
本节目标:学会用聚合函数对数据做统计(计数、求和、平均、最大最小),用 GROUP BY 按维度分组统计,用 HAVING 过滤分组结果,用 WITH ROLLUP 生成分组汇总行。
23.1 什么是聚合函数
前面的查询都是逐行返回数据。但很多时候你要的不是每行数据,而是一个统计结果:
- users 表里有多少个用户?
- orders 表的订单总金额是多少?
- 用户的平均年龄是多少?
聚合函数(Aggregate Function)就是把这些多行数据”聚”成一个值的函数。MySQL 有五个核心聚合函数:
| 函数 | 作用 | 示例 |
|---|---|---|
COUNT() | 计数 | COUNT(*) 总行数 |
SUM() | 求和 | SUM(amount) 金额总和 |
AVG() | 平均值 | AVG(age) 平均年龄 |
MAX() | 最大值 | MAX(amount) 最大金额 |
MIN() | 最小值 | MIN(age) 最小年龄 |
23.2 COUNT 的几种用法
COUNT 是用得最多的聚合函数,但它有几种写法,含义不同:
-- 1. COUNT(*):统计所有行数,包括 NULL
SELECT COUNT(*) FROM users;
-- 2. COUNT(列名):统计该列非 NULL 的行数
SELECT COUNT(email) FROM users;
-- 3. COUNT(DISTINCT 列名):统计不重复的非 NULL 值数量
SELECT COUNT(DISTINCT age) FROM users;
三者区别:
| 写法 | 统计什么 | 包含 NULL |
|---|---|---|
COUNT(*) | 所有行 | 包含 |
COUNT(列名) | 该列有值的行 | 不包含 |
COUNT(DISTINCT 列名) | 该列不同值的数量 | 不包含 |
Note
COUNT(*)和COUNT(1)效果完全一样,都是统计所有行。MySQL 优化器对两者处理方式相同,没有性能差异。用哪个都行,但COUNT(*)是 SQL 标准写法,更通用。
COUNT 不忽略 NULL 的陷阱
SELECT COUNT(*) AS 总行数,
COUNT(email) AS 有邮箱的,
COUNT(age) AS 有年龄的
FROM users;
如果有些行的 email 或 age 是 NULL,COUNT(email) 和 COUNT(age) 会比 COUNT(*) 小。这个差异必须注意。
Warning别用
COUNT(列名)代替COUNT(*)来统计总行数。如果那列有 NULL,结果会偏小。想数行数就用COUNT(*)。
23.3 SUM、AVG、MAX、MIN
这几个更直白:
-- 订单总金额
SELECT SUM(amount) AS 总金额 FROM orders;
-- 用户平均年龄
SELECT AVG(age) AS 平均年龄 FROM users;
-- 最大订单金额
SELECT MAX(amount) AS 最大金额 FROM orders;
-- 最小订单金额
SELECT MIN(amount) AS 最小金额 FROM orders;
-- 一次查多个统计值
SELECT
COUNT(*) AS 订单数,
SUM(amount) AS 总金额,
AVG(amount) AS 平均金额,
MAX(amount) AS 最大金额,
MIN(amount) AS 最小金额
FROM orders
WHERE status = 'paid';
Tip聚合函数会自动忽略 NULL。
SUM(amount)不会因为某行 amount 是 NULL 而报错,它只对非 NULL 的行求和。AVG(age)也是只算有年龄的行的平均值。这跟 COUNT 的行为一致。
SUM 和 AVG 的精度问题
SELECT AVG(age) FROM users;
-- 结果可能是 26.3333,小数位很多
AVG 返回的是精确小数。如果只想要整数,套一层 ROUND:
SELECT ROUND(AVG(age), 0) AS 平均年龄 FROM users;
对于金额,用 DECIMAL 类型存储而不是 FLOAT,避免浮点精度问题。
23.4 GROUP BY:分组统计
聚合函数单独用,是对整张表做统计。配合 GROUP BY,可以按维度分组,每组分别统计。
-- 按订单状态分组,统计每种状态有多少订单
SELECT status, COUNT(*) AS 订单数
FROM orders
GROUP BY status;
结果类似:
+---------+--------+
| status | 订单数 |
+---------+--------+
| paid | 150 |
| pending | 80 |
| shipped | 45 |
| refunded| 15 |
+---------+--------+
再来几个例子:
-- 每个用户的订单总金额
SELECT user_id, SUM(amount) AS 总消费
FROM orders
GROUP BY user_id;
-- 每个年龄段的用户数
SELECT age, COUNT(*) AS 人数
FROM users
GROUP BY age
ORDER BY age;
多列分组
GROUP BY 可以按多列分组,实现更细的维度统计:
-- 按用户和订单状态分组,看每个用户每种状态的订单数
SELECT user_id, status, COUNT(*) AS 订单数
FROM orders
GROUP BY user_id, status;
先按 user_id 分组,user_id 相同的再按 status 分组。
NoteSELECT 中出现的列,要么在 GROUP BY 里,要么被聚合函数包裹。这是 SQL 标准的要求。MySQL 8.0+ 默认开启
ONLY_FULL_GROUP_BY模式,不遵守会报错。5.7 默认不检查,但行为是不可靠的。-- 8.0+ 会报错:username 不在 GROUP BY 里,也没被聚合 SELECT username, COUNT(*) FROM users GROUP BY age; -- ERROR 1055: 'username' isn't in GROUP BY
23.5 HAVING:过滤分组
WHERE 用来过滤行,在分组之前执行。如果想过滤分组之后的结果呢?用 HAVING。
-- 找消费总额超过 1000 的用户
SELECT user_id, SUM(amount) AS 总消费
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 1000;
HAVING 跟 WHERE 的核心区别:
| 对比项 | WHERE | HAVING |
|---|---|---|
| 执行时机 | 分组前 | 分组后 |
| 能用聚合函数 | 不能 | 能 |
| 过滤对象 | 行 | 分组 |
-- WHERE 和 HAVING 配合使用
SELECT user_id, SUM(amount) AS 总消费
FROM orders
WHERE status = 'paid' -- 先过滤:只要已支付的订单
GROUP BY user_id -- 再分组:按用户分组
HAVING SUM(amount) > 1000; -- 最后过滤:消费超 1000 的
执行顺序:WHERE -> GROUP BY -> HAVING。先把 paid 的行筛出来,再按用户分组统计,最后过滤掉总额不超过 1000 的组。
Warning别把该用 HAVING 的条件写到 WHERE 里。
WHERE SUM(amount) > 1000会报错,因为 WHERE 执行时还没分组,聚合函数还没算出来。
23.6 GROUP BY 的注意事项
GROUP BY 中的 NULL
如果分组列有 NULL,NULL 会被当作一个独立的分组:
SELECT age, COUNT(*) FROM users GROUP BY age;
-- 结果里会有一行 age 为 NULL,统计所有 age 为 NULL 的用户
GROUP BY 的默认排序
MySQL 8.0 之前,GROUP BY 默认会按分组列排序。8.0+ 不再保证排序,如果需要有序结果,显式加 ORDER BY:
SELECT status, COUNT(*) AS 订单数
FROM orders
GROUP BY status
ORDER BY 订单数 DESC;
不要用 GROUP BY 来去重
虽然 GROUP BY 确实有去重效果,但去重应该用 DISTINCT,语义更清晰。GROUP BY 的语义是”分组统计”,不是”去重”。
TipGROUP BY 配合聚合函数才有意义。如果你只是想去重,用 DISTINCT。如果你要统计每组的数据,用 GROUP BY + 聚合函数。让每种语法各司其职。
23.7 WITH ROLLUP:分组汇总
WITH ROLLUP 在分组统计的基础上,额外生成一行”汇总行”:
SELECT status, COUNT(*) AS 订单数, SUM(amount) AS 总金额
FROM orders
GROUP BY status WITH ROLLUP;
结果:
+---------+--------+-----------+
| status | 订单数 | 总金额 |
+---------+--------+-----------+
| paid | 150 | 45000.00 |
| pending | 80 | 16000.00 |
| shipped | 45 | 13500.00 |
| refunded| 15 | 3000.00 |
| NULL | 290 | 77500.00 | <- 汇总行
+---------+--------+-----------+
最后一行 status 为 NULL,是所有分组的汇总:总订单数 290,总金额 77500。
多列分组时,ROLLUP 会为每个分组层级都生成汇总行:
SELECT user_id, status, COUNT(*) AS 订单数
FROM orders
GROUP BY user_id, status WITH ROLLUP;
NoteROLLUP 生成的汇总行中,分组列的值是 NULL。如果原数据本身就有 NULL,容易混淆。可以用
IFNULL或COALESCE给汇总行一个标识:SELECT IFNULL(status, '全部') AS 状态, COUNT(*) AS 订单数 FROM orders GROUP BY status WITH ROLLUP;
23.8 GROUP_CONCAT:分组拼接
GROUP_CONCAT 是 MySQL 特有的聚合函数,把分组内的值拼接成一个字符串:
-- 查每个用户的订单状态,用逗号拼起来
SELECT user_id, GROUP_CONCAT(status) AS 状态列表
FROM orders
GROUP BY user_id;
结果:
+---------+-------------------------+
| user_id | 状态列表 |
+---------+-------------------------+
| 1 | paid,paid,shipped |
| 2 | pending,paid |
| 3 | refunded |
+---------+-------------------------+
可以指定分隔符、去重、排序:
SELECT
user_id,
GROUP_CONCAT(DISTINCT status ORDER BY status SEPARATOR ' | ') AS 状态列表
FROM orders
GROUP BY user_id;
Warning
GROUP_CONCAT默认有长度限制(group_concat_max_len,默认 1024 字节)。拼接结果超过这个长度会被截断。如果数据多,需要在查询前设置:SET SESSION group_concat_max_len = 1000000;
23.9 小结
聚合函数把多行变成一个值,GROUP BY 把数据按维度分组后分别聚合,HAVING 过滤分组结果。核心记住:COUNT(*) 数行数、COUNT(列) 数非空值、WHERE 过滤行而 HAVING 过滤分组、WITH ROLLUP 生成汇总行、SELECT 的非聚合列必须在 GROUP BY 里。下一章学子查询,在查询里嵌套查询。