MySQL中五大基础聚合函数的区别与应用
作者:花生了什么事o
聚合函数是什么
聚合函数是对一组行做计算,返回单个值的函数。
MySQL 提供了以下五个最常用的聚合函数:
| 函数 | 作用 |
|---|---|
COUNT() | 统计行数 |
SUM() | 求和 |
AVG() | 求平均值 |
MAX() | 求最大值 |
MIN() | 求最小值 |
这五个函数就足以覆盖日常开发中 90% 以上的统计需求。下面逐个拆开来讲。
五大基础聚合函数
COUNT:数行数
COUNT() 用来统计满足条件的行数。它有三种常见写法,面试和日常开发中经常被混为一谈:
SELECT COUNT(*) FROM orders; SELECT COUNT(1) FROM orders; SELECT COUNT(status) FROM orders;
这三种写法看起来差不多,实际行为有明确的区别。
COUNT(*) vs COUNT(1)
先说结论:COUNT(*) 和 COUNT(1) 在 MySQL 中完全等价,性能没有任何区别。
COUNT(*) 的 * 不是"取出所有列"的意思,它只是一个语法符号,表示"统计行数"。COUNT(1) 中的 1 也不是取第一列,而是对每一行计算常量表达式 1,然后计数。MySQL 的查询优化器会把两者统一处理,生成完全相同的执行计划。
你可以用 EXPLAIN 验证:
EXPLAIN SELECT COUNT(*) FROM orders; EXPLAIN SELECT COUNT(1) FROM orders; -- 两条语句的 Extra、rows、key 完全一致
所以不用纠结用哪个,选 COUNT(*) 就好,它是 SQL 标准写法,可读性也最好。
COUNT(列名) 的行为
COUNT(列名) 和前两个有本质区别:它只统计该列不为 NULL 的行。
-- 假设 orders 表有 100 行,其中 status 列有 5 个 NULL SELECT COUNT(*) FROM orders; -- 结果:100 SELECT COUNT(1) FROM orders; -- 结果:100 SELECT COUNT(status) FROM orders; -- 结果:95
COUNT(status) 遍历每一行时,会检查 status 是否为 NULL,是就跳过。这意味着 COUNT(列名) 天然会排除 NULL 行,在统计"有多少行这个字段有值"时很有用,但如果本意是统计总行数,用它就会少数。
三者对比
| 写法 | 统计范围 | NULL 处理 | 性能 |
|---|---|---|---|
COUNT(*) | 总行数 | 不受 NULL 影响 | 最优(MySQL 优化) |
COUNT(1) | 总行数 | 不受 NULL 影响 | 和 COUNT(*) 一样 |
COUNT(列名) | 该列非 NULL 的行数 | 跳过 NULL | 一样(但语义不同) |
总结:统计总行数用 COUNT(*) 或 COUNT(1),统计某列有值的行数用 COUNT(列名)。
COUNT(DISTINCT 列名)
还有一个常见写法,用来统计去重后的行数:
-- 有多少个不同的用户下过单 SELECT COUNT(DISTINCT user_id) FROM orders;
COUNT(DISTINCT 列名) 先去重再计数,NULL 不参与——即使有多行 NULL,也只算一个(实际上 MySQL 的 DISTINCT 会直接跳过 NULL)。
性能提醒
COUNT(*) 在 InnoDB 中需要遍历索引来计数,大表上执行会比较慢。如果只是想要一个近似值,可以查 information_schema.TABLES 里的 TABLE_ROWS 字段,那是 InnoDB 的估算值,速度极快但不精确。
-- 快速获取近似行数 SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'orders';
SUM 和 AVG:求和与平均
SUM() 对数值列求和,AVG() 求平均值。用法很直观:
-- 所有订单的总金额 SELECT SUM(amount) FROM orders; -- 所有订单的平均金额 SELECT AVG(amount) FROM orders;
有一个细节容易忽略:AVG() 的计算公式是 SUM() / COUNT(列名),而不是 SUM() / COUNT(*)。 如果列里有 NULL,NULL 不参与分子也不参与分母。
-- 假设 amount 列有值:100, 200, NULL, 300 -- SUM(amount) = 600(NULL 被忽略) -- AVG(amount) = 600 / 3 = 200(不是 600 / 4 = 150)
如果你期望 NULL 当作 0 来参与计算,需要用 IFNULL 或 COALESCE:
SELECT AVG(IFNULL(amount, 0)) FROM orders; -- 现在 NULL 被当作 0,结果是 600 / 4 = 150
MAX 和 MIN:极值查询
MAX() 和 MIN() 分别返回一组数据中的最大值和最小值:
-- 最大的订单金额 SELECT MAX(amount) FROM orders; -- 最早的订单时间 SELECT MIN(create_time) FROM orders;
它们不仅适用于数值,也适用于字符串和日期。字符串比较的是字典序,日期比较的是时间先后。
一个常见误区: 很多人以为 MAX 和 MIN 只能用在 SELECT 里,其实它们在子查询中更有价值:
-- 找出金额最大的那条订单的完整信息 SELECT * FROM orders WHERE amount = (SELECT MAX(amount) FROM orders);
不过这种写法有一个隐患:如果有多条订单金额相同且都是最大值,会返回多行。如果你只想要一条,加 LIMIT 1。
GROUP BY:把数据切成一块一块再聚合
单个聚合函数只能算出"全局汇总"。但实际业务中,我们更常需要"分组统计"——比如每个用户的订单数、每个月的销售额、每个状态的订单量。
这就是 GROUP BY 的作用:按某个列(或多个列)把数据分成若干组,每组独立做聚合。
-- 每个用户的订单数
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id;
-- 每个月的销售总额
SELECT
DATE_FORMAT(create_time, '%Y-%m') AS month,
SUM(amount) AS monthly_total
FROM orders
GROUP BY DATE_FORMAT(create_time, '%Y-%m');
执行逻辑可以用一句话概括:先分组,再聚合,每组出一行结果。
SELECT 中出现的非聚合列,必须出现在 GROUP BY 中。 这是 SQL 的基本规则。比如下面这条就是错误的:
-- 错误:status 没有出现在 GROUP BY 中,也不是聚合函数的参数 SELECT user_id, status, COUNT(*) FROM orders GROUP BY user_id; -- 正确:status 也加入分组 SELECT user_id, status, COUNT(*) FROM orders GROUP BY user_id, status;
MySQL 在 ONLY_FULL_GROUP_BY 模式下会直接拒绝这种写法。关闭这个模式虽然能执行,但结果是不确定的:status 取的是每组中哪一行的值,MySQL 自己也说不清。
WHERE vs HAVING:过滤时机不一样
分组之后,你可能想对结果做进一步筛选。比如"只看订单数超过 10 的用户"。这时候有两个选择:WHERE 和 HAVING。
它们的核心区别在于过滤时机:
| WHERE | HAVING | |
|---|---|---|
| 过滤时机 | 分组之前 | 分组之后 |
| 能否用聚合函数 | 不能 | 能 |
| 操作对象 | 原始行 | 分组后的结果集 |
-- WHERE:在分组前过滤原始行 SELECT user_id, COUNT(*) AS order_count FROM orders WHERE status = 'paid' -- 先过滤掉未支付的订单 GROUP BY user_id; -- HAVING:在分组后过滤聚合结果 SELECT user_id, COUNT(*) AS order_count FROM orders GROUP BY user_id HAVING COUNT(*) > 10; -- 再过滤掉订单数不超过 10 的组
如果你同时需要 WHERE 和 HAVING,顺序是固定的:WHERE 在前,GROUP BY 在中,HAVING 在后。
-- 完整的执行顺序:WHERE → GROUP BY → HAVING → ORDER BY → LIMIT SELECT user_id, SUM(amount) AS total FROM orders WHERE status = 'paid' -- 第一步:过滤原始行 GROUP BY user_id -- 第二步:分组 HAVING total > 1000 -- 第三步:过滤分组结果 ORDER BY total DESC -- 第四步:排序 LIMIT 10; -- 第五步:取前 10 条
一个常见的错误: 在 WHERE 里使用聚合函数。
-- 错误:WHERE 还没到分组阶段,不能用聚合函数 SELECT user_id, COUNT(*) FROM orders WHERE COUNT(*) > 10 GROUP BY user_id; -- 正确:用 HAVING SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING COUNT(*) > 10;
逻辑上也说得通:WHERE 是逐行过滤,COUNT(*) 是一组行的统计结果,逐行阶段根本不知道"这一组有多少行",所以没法用。
聚合函数的嵌套与子查询
聚合函数可以嵌套使用,但有一条铁律:聚合函数不能嵌套在 WHERE 子句中直接使用,必须放在子查询里。
-- 错误:MySQL 不允许在 WHERE 中直接用聚合函数 SELECT * FROM orders WHERE amount > AVG(amount); -- 正确:用子查询包一层 SELECT * FROM orders WHERE amount > (SELECT AVG(amount) FROM orders);
同样,聚合函数可以和 GROUP BY 结合后作为子查询使用:
-- 找出每个用户消费最高的那笔订单
SELECT o.*
FROM orders o
INNER JOIN (
SELECT user_id, MAX(amount) AS max_amount
FROM orders
GROUP BY user_id
) t ON o.user_id = t.user_id AND o.amount = t.max_amount;
这种"先聚合再关联"的模式在报表统计中非常常见。先在子查询中算出每组的聚合值,再和原表 JOIN 取出完整行。
小结
聚合函数的本质就是把多行数据压缩成一个值。 COUNT 数行数,SUM 算总和,AVG 算平均,MAX 和 MIN 找极值。配合 GROUP BY 可以按字段维度分组统计,配合 HAVING 可以对分组结果再过滤。
到此这篇关于MySQL中五大基础聚合函数的区别与应用的文章就介绍到这了,更多相关MySQL聚合函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
