写在前面
大家好,欢迎来到MySQL全面教学系列的第5天!经过前面4天的学习,我们已经掌握了MySQL的基础操作、数据类型、表的创建与管理,以及单表查询的核心技能。今天,我们将进入数据分析的核心领域------聚合函数与分组查询。
在实际工作中,数据统计和分析是最常见的需求。无论是统计用户数量、计算销售额、还是分析订单趋势,都离不开聚合函数和GROUP BY。掌握这些技能,你就能从海量数据中提炼出有价值的信息。
让我们开始今天的学习之旅!

目录
-
- 写在前面
- 一、常用聚合函数
-
- [1.1 五大核心聚合函数](#1.1 五大核心聚合函数)
- [1.2 COUNT函数详解](#1.2 COUNT函数详解)
- [1.3 SUM、AVG、MAX、MIN实战](#1.3 SUM、AVG、MAX、MIN实战)
- [二、GROUP BY分组](#二、GROUP BY分组)
-
- [2.1 单字段分组](#2.1 单字段分组)
- [2.2 多字段分组](#2.2 多字段分组)
- [2.3 分组后筛选HAVING](#2.3 分组后筛选HAVING)
- [2.4 完整执行顺序](#2.4 完整执行顺序)
- [三、WITH ROLLUP分组小计](#三、WITH ROLLUP分组小计)
- 四、实战:电商数据统计
-
- [4.1 订单量统计](#4.1 订单量统计)
- [4.2 销售额统计](#4.2 销售额统计)
- [4.3 用户统计](#4.3 用户统计)
- 五、踩坑提醒与经验之谈
-
- [5.1 SELECT中出现非聚合字段](#5.1 SELECT中出现非聚合字段)
- [5.2 HAVING和WHERE混用](#5.2 HAVING和WHERE混用)
- [5.3 NULL值处理](#5.3 NULL值处理)
- 六、面试高频考点
-
- [6.1 WHERE和HAVING的执行顺序?](#6.1 WHERE和HAVING的执行顺序?)
- [6.2 GROUP BY后SELECT能写什么?](#6.2 GROUP BY后SELECT能写什么?)
- [6.3 COUNT(*)和COUNT(1)有区别吗?](#6.3 COUNT(*)和COUNT(1)有区别吗?)
- [6.4 如何统计多列的NULL和非NULL数量?](#6.4 如何统计多列的NULL和非NULL数量?)
- [6.5 GROUP BY后如何对分组结果排序?](#6.5 GROUP BY后如何对分组结果排序?)
- 七、总结
- 参考资料
- 互动话题
一、常用聚合函数
聚合函数(Aggregate Functions)用于对一组值进行计算,并返回单个值。它们是数据分析的基石。
1.1 五大核心聚合函数
| 函数 | 作用 | 返回值类型 | 忽略NULL值 |
|---|---|---|---|
| COUNT() | 统计记录数 | 整数 | 视情况而定 |
| SUM() | 求和 | 数值 | 是 |
| AVG() | 平均值 | 数值 | 是 |
| MAX() | 最大值 | 原数据类型 | 是 |
| MIN() | 最小值 | 原数据类型 | 是 |
1.2 COUNT函数详解
COUNT是最常用的聚合函数,但很多人对它的用法存在误解。
sql
-- 统计表中所有记录数(包括NULL)
SELECT COUNT(*) FROM orders;
-- 统计指定字段非NULL的记录数
SELECT COUNT(user_id) FROM orders;
-- 统计去重后的记录数
SELECT COUNT(DISTINCT user_id) FROM orders;
COUNT(*) vs COUNT(字段)的区别:
| 对比项 | COUNT(*) | COUNT(字段) |
|---|---|---|
| 统计范围 | 所有行 | 字段非NULL的行 |
| 性能 | 通常更快(MySQL优化) | 需要判断NULL |
| 使用场景 | 统计总数 | 统计有值的记录 |
| 结果差异 | 包含NULL行 | 排除NULL行 |
经验之谈: 如果要统计表的总行数,优先使用COUNT(*),MySQL对此有特殊优化。
1.3 SUM、AVG、MAX、MIN实战
假设我们有以下订单表:
sql
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
order_date DATE
);
INSERT INTO orders VALUES
(1, 101, 199.99, '2024-01-01'),
(2, 102, 299.50, '2024-01-02'),
(3, 101, 150.00, '2024-01-03'),
(4, 103, NULL, '2024-01-04');
sql
-- 统计总销售额
SELECT SUM(amount) AS total_sales FROM orders;
-- 结果:649.49(NULL被忽略)
-- 计算平均订单金额
SELECT AVG(amount) AS avg_amount FROM orders;
-- 结果:216.50(只计算3条非NULL记录)
-- 找出最大和最小订单金额
SELECT
MAX(amount) AS max_amount,
MIN(amount) AS min_amount
FROM orders;
-- 组合使用:全面的数据统计
SELECT
COUNT(*) AS total_orders,
COUNT(amount) AS valid_orders,
SUM(amount) AS total_sales,
AVG(amount) AS avg_amount,
MAX(amount) AS max_amount,
MIN(amount) AS min_amount
FROM orders;
踩坑提醒: AVG函数会自动忽略NULL值,但计算平均值时只基于非NULL的记录数。如果你想把NULL当作0计算,需要使用AVG(IFNULL(amount, 0))。
二、GROUP BY分组
GROUP BY用于将数据按一个或多个字段分组,然后对每组应用聚合函数。
2.1 单字段分组
sql
-- 按用户统计订单数量和总消费
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id;
2.2 多字段分组
sql
-- 按年份和月份统计销售额
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
COUNT(*) AS order_count,
SUM(amount) AS monthly_sales
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date)
ORDER BY year, month;
2.3 分组后筛选HAVING
WHERE子句在分组前过滤数据,HAVING在分组后过滤数据。
sql
-- 找出消费超过500元的用户
SELECT
user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_spent
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 500;
WHERE vs HAVING对比:
| 特性 | WHERE | HAVING |
|---|---|---|
| 执行时机 | 分组前 | 分组后 |
| 过滤对象 | 原始行 | 分组后的结果 |
| 可用条件 | 任意列 | 聚合函数或GROUP BY字段 |
| 性能 | 先过滤,数据量小 | 后过滤,数据量大 |
2.4 完整执行顺序
理解SQL的执行顺序对写出正确的查询至关重要:
1. FROM -- 确定数据来源
2. WHERE -- 过滤原始数据
3. GROUP BY -- 分组
4. HAVING -- 过滤分组结果
5. SELECT -- 选择列
6. ORDER BY -- 排序
7. LIMIT -- 限制返回行数
三、WITH ROLLUP分组小计
WITH ROLLUP用于在分组结果中添加小计和总计行。
sql
-- 按年份统计销售额,并显示总计
SELECT
YEAR(order_date) AS year,
COUNT(*) AS order_count,
SUM(amount) AS total_sales
FROM orders
GROUP BY YEAR(order_date) WITH ROLLUP;
结果示例:
| year | order_count | total_sales |
|---|---|---|
| 2023 | 150 | 45000.00 |
| 2024 | 200 | 68000.00 |
| NULL | 350 | 113000.00 |
多字段ROLLUP:
sql
-- 按年份和月份分组,显示各级小计
SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
SUM(amount) AS sales
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date) WITH ROLLUP;
踩坑提醒: ROLLUP产生的总计行中,分组字段显示为NULL。如果你的数据本身就有NULL值,可能需要使用GROUPING()函数来区分。
sql
-- 使用GROUPING函数区分NULL类型
SELECT
YEAR(order_date) AS year,
GROUPING(YEAR(order_date)) AS is_rollup,
SUM(amount) AS sales
FROM orders
GROUP BY YEAR(order_date) WITH ROLLUP;
四、实战:电商数据统计
假设我们有一个电商系统,包含以下表结构:
sql
-- 用户表
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50),
register_date DATE,
city VARCHAR(50)
);
-- 订单表
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
order_amount DECIMAL(10,2),
order_status ENUM('pending', 'paid', 'shipped', 'completed', 'cancelled'),
create_time DATETIME
);
-- 订单商品表
CREATE TABLE order_items (
item_id INT PRIMARY KEY,
order_id INT,
product_name VARCHAR(100),
quantity INT,
unit_price DECIMAL(10,2)
);
4.1 订单量统计
sql
-- 每日订单量统计
SELECT
DATE(create_time) AS order_date,
COUNT(*) AS order_count,
COUNT(DISTINCT user_id) AS unique_users,
SUM(order_amount) AS daily_revenue
FROM orders
WHERE order_status != 'cancelled'
GROUP BY DATE(create_time)
ORDER BY order_date DESC;
-- 订单状态分布
SELECT
order_status,
COUNT(*) AS count,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS percentage
FROM orders
GROUP BY order_status;
4.2 销售额统计
sql
-- 月度销售趋势
SELECT
DATE_FORMAT(create_time, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(order_amount) AS revenue,
AVG(order_amount) AS avg_order_value
FROM orders
WHERE order_status IN ('paid', 'shipped', 'completed')
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
ORDER BY month;
-- 城市销售排名TOP10
SELECT
u.city,
COUNT(DISTINCT o.user_id) AS buyer_count,
COUNT(*) AS order_count,
SUM(o.order_amount) AS total_revenue
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.order_status != 'cancelled'
GROUP BY u.city
ORDER BY total_revenue DESC
LIMIT 10;
4.3 用户统计
sql
-- 用户消费分层(RFM模型简化版)
SELECT
CASE
WHEN total_spent >= 10000 THEN '高价值用户'
WHEN total_spent >= 5000 THEN '中价值用户'
WHEN total_spent >= 1000 THEN '普通用户'
ELSE '低价值用户'
END AS user_segment,
COUNT(*) AS user_count,
AVG(total_spent) AS avg_spent
FROM (
SELECT
user_id,
SUM(order_amount) AS total_spent
FROM orders
WHERE order_status != 'cancelled'
GROUP BY user_id
) t
GROUP BY user_segment;
-- 新用户注册趋势
SELECT
DATE_FORMAT(register_date, '%Y-%m') AS month,
COUNT(*) AS new_users
FROM users
GROUP BY DATE_FORMAT(register_date, '%Y-%m')
ORDER BY month;
五、踩坑提醒与经验之谈
5.1 SELECT中出现非聚合字段
错误示例:
sql
-- 错误!username不在GROUP BY中
SELECT username, COUNT(*)
FROM orders o
JOIN users u ON o.user_id = u.user_id
GROUP BY o.user_id;
在MySQL 5.7+的严格模式下,上述SQL会报错。只有以下字段可以出现在SELECT中:
- GROUP BY中的字段
- 聚合函数的结果
- 函数依赖的字段(如主键)
正确写法:
sql
SELECT u.user_id, u.username, COUNT(*)
FROM orders o
JOIN users u ON o.user_id = u.user_id
GROUP BY u.user_id, u.username;
5.2 HAVING和WHERE混用
常见错误:
sql
-- 低效写法
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id
HAVING order_date > '2024-01-01'; -- 错误!HAVING不能用原始字段
正确写法:
sql
-- 高效写法
SELECT user_id, SUM(amount)
FROM orders
WHERE order_date > '2024-01-01' -- 先过滤
GROUP BY user_id;
经验之谈: 能用WHERE过滤的,绝不要用HAVING。WHERE在分组前过滤,减少参与分组的数据量;HAVING在分组后过滤,数据量更大。
5.3 NULL值处理
sql
-- 统计有邮箱的用户数量
SELECT COUNT(email) FROM users; -- 排除NULL
-- 统计所有用户,没有邮箱的显示0
SELECT COUNT(IFNULL(email, '')) FROM users;
-- 分组时NULL会被当作一个组
SELECT city, COUNT(*) FROM users GROUP BY city;
-- NULL会单独显示为一行
六、面试高频考点
6.1 WHERE和HAVING的执行顺序?
答案: WHERE在GROUP BY之前执行,HAVING在GROUP BY之后执行。
执行顺序:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
6.2 GROUP BY后SELECT能写什么?
答案:
- GROUP BY中的字段
- 聚合函数(COUNT、SUM、AVG等)
- 与GROUP BY字段有函数依赖的字段(如主键对应的非主键字段)
在MySQL中,如果启用了ONLY_FULL_GROUP_BY模式,SELECT列表中的非聚合字段必须出现在GROUP BY子句中。
6.3 COUNT(*)和COUNT(1)有区别吗?
答案: 在MySQL中没有区别,两者性能相同。COUNT(*)是标准SQL语法,推荐优先使用。
6.4 如何统计多列的NULL和非NULL数量?
sql
SELECT
COUNT(*) AS total,
COUNT(col1) AS col1_not_null,
COUNT(*) - COUNT(col1) AS col1_null,
COUNT(col2) AS col2_not_null,
COUNT(*) - COUNT(col2) AS col2_null
FROM table_name;
6.5 GROUP BY后如何对分组结果排序?
sql
-- 按聚合结果排序
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
ORDER BY cnt DESC;
-- 按多个字段排序
SELECT city, COUNT(*) AS cnt
FROM users
GROUP BY city
ORDER BY cnt DESC, city ASC;
七、总结
今天我们学习了MySQL聚合函数与分组查询的核心知识:
- 聚合函数:COUNT、SUM、AVG、MAX、MIN的使用方法和注意事项
- GROUP BY:单字段和多字段分组,以及分组后的数据筛选
- HAVING:分组后的过滤条件,与WHERE的区别
- WITH ROLLUP:生成分组小计和总计
- 实战应用:电商系统的订单量、销售额、用户统计
下一步预告
Day6:MySQL多表查询与JOIN
明天我们将学习多表查询的核心技术------JOIN。从INNER JOIN到LEFT JOIN,从自连接到UNION,你将掌握如何在多个表之间进行数据关联查询。这是实际工作中最常用的技能之一,敬请期待!
参考资料
MySQL 8.0 Reference Manual - Aggregate Functions
互动话题
- 你在使用GROUP BY时遇到过哪些坑?欢迎在评论区分享!
- 你们公司的数据分析场景主要用哪些聚合函数?
- 对于HAVING和WHERE的区别,你有什么独特的理解方式?
如果觉得本文对你有帮助,请点赞收藏!明天见!