【MySQL全面教学】MySQL聚合函数与分组Day5(2026年)

写在前面

大家好,欢迎来到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聚合函数与分组查询的核心知识:

  1. 聚合函数:COUNT、SUM、AVG、MAX、MIN的使用方法和注意事项
  2. GROUP BY:单字段和多字段分组,以及分组后的数据筛选
  3. HAVING:分组后的过滤条件,与WHERE的区别
  4. WITH ROLLUP:生成分组小计和总计
  5. 实战应用:电商系统的订单量、销售额、用户统计

下一步预告

Day6:MySQL多表查询与JOIN

明天我们将学习多表查询的核心技术------JOIN。从INNER JOIN到LEFT JOIN,从自连接到UNION,你将掌握如何在多个表之间进行数据关联查询。这是实际工作中最常用的技能之一,敬请期待!


参考资料

MySQL 8.0 Reference Manual - Aggregate Functions


互动话题

  1. 你在使用GROUP BY时遇到过哪些坑?欢迎在评论区分享!
  2. 你们公司的数据分析场景主要用哪些聚合函数?
  3. 对于HAVING和WHERE的区别,你有什么独特的理解方式?

如果觉得本文对你有帮助,请点赞收藏!明天见!

相关推荐
星栈独行9 小时前
别让 API 跳去登录页:我在 Axum 里做了认证失败双通道
前端·后端·rust·开源·github·个人开发
JaguarJack9 小时前
TrueAsync Server 为 PHP 带来了原生的高性能 HTTP 服务器
后端·php
牛马鸡niumasi9 小时前
Mysql:事务管理(中)
数据库·mysql
字节高级特工9 小时前
Redis事务:简单但实用的打包执行
数据库·redis·后端·缓存
极客小云9 小时前
【用 Go 写一个统一的 LLM Token 统计库:tokencalc 的设计与实现】
开发语言·后端·golang
GISer_Jing9 小时前
后端系统稳定性基石:数据库设计、接口幂等性与边界case处理全链路实战
数据库·oracle·架构
无忧.芙桃9 小时前
MySQL安装与基础操作指南
数据库
Vect__9 小时前
C++转go的之路:变量声明、iota、函数、切片、init、defer
开发语言·后端·golang
fengxin_rou9 小时前
【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
spring boot·后端·elasticsearch