首页 / MySQL 入门教程 / 聚合函数与分组统计

MySQL 入门教程

聚合函数与分组统计

本教程共 46 篇 · 第 23 篇 · 更新于 2026-07-30 · 约 11 分钟阅读

MySQLMySQL 入门教程聚合函数GROUP BYHAVINGCOUNTSUMWITH ROLLUP

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 分组。

Note

SELECT 中出现的列,要么在 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 的核心区别:

对比项WHEREHAVING
执行时机分组前分组后
能用聚合函数不能
过滤对象分组
-- 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 的语义是”分组统计”,不是”去重”。

Tip

GROUP 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;
Note

ROLLUP 生成的汇总行中,分组列的值是 NULL。如果原数据本身就有 NULL,容易混淆。可以用 IFNULLCOALESCE 给汇总行一个标识:

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 里。下一章学子查询,在查询里嵌套查询。