MySQL数据查询中GROUP BY的用法与实战详解
什么是 GROUP BY
GROUP BY 是 MySQL 中最常用的分组查询语法,用来按照一个或多个字段对数据进行分组,然后通常配合聚合函数一起使用,比如:
COUNT():统计数量SUM():求和AVG():平均值MAX():最大值MIN():最小值
它解决的不是“查出每一行”,而是“按某种维度汇总数据”。
例如:
- 按部门统计员工人数
- 按用户统计订单数和消费总额
- 按日期统计每天的访问量
GROUP BY 的核心作用
普通查询返回的是明细数据,而 GROUP BY 返回的是分组后的结果。
看一个直观例子。假设有一张订单表:
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
product_name VARCHAR(100) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT NOT NULL,
create_time DATETIME NOT NULL
);
插入几条数据:
INSERT INTO orders (user_id, product_name, amount, status, create_time) VALUES
(1, '键盘', 199.00, 1, '2026-04-01 10:00:00'),
(1, '鼠标', 99.00, 1, '2026-04-01 11:00:00'),
(2, '显示器', 1299.00, 1, '2026-04-02 09:30:00'),
(2, '耳机', 299.00, 0, '2026-04-02 15:00:00'),
(3, '笔记本支架', 159.00, 1, '2026-04-03 08:20:00');
如果直接查询:
SELECT * FROM orders;
返回的是每一条订单明细。
但如果按用户分组统计订单数:
SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id;
结果含义就变成了:每个用户有多少笔订单。
GROUP BY 的基本语法
SELECT 分组字段, 聚合函数
FROM 表名
WHERE 条件
GROUP BY 分组字段
HAVING 分组后条件
ORDER BY 排序字段;
执行顺序通常可以理解为:
FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT
这里最重要的是两个点:
WHERE是分组前过滤HAVING是分组后过滤
单字段分组
按用户统计订单数
SELECT user_id, COUNT(*) AS total_orders
FROM orders
GROUP BY user_id;
按订单状态统计数量
SELECT status, COUNT(*) AS total
FROM orders
GROUP BY status;
这个查询的意义是:把 status 相同的数据归为一组,然后统计每组的记录数。
多字段分组
GROUP BY 不只能按一个字段分组,也可以按多个字段组合分组。
按用户和状态分组统计订单数
SELECT user_id, status, COUNT(*) AS total
FROM orders
GROUP BY user_id, status;
这和只按 user_id 分组完全不同。
GROUP BY user_id:每个用户一组GROUP BY user_id, status:每个用户的每种状态分别一组
也就是说,分组粒度更细。
常见聚合函数与 GROUP BY 配合使用
1. COUNT()
统计数量:
SELECT user_id, COUNT(*) AS total_orders
FROM orders
GROUP BY user_id;
2. SUM()
统计总金额:
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id;
3. AVG()
统计平均订单金额:
SELECT user_id, AVG(amount) AS avg_amount
FROM orders
GROUP BY user_id;
4. MAX() / MIN()
统计最大值和最小值:
SELECT user_id, MAX(amount) AS max_amount, MIN(amount) AS min_amount
FROM orders
GROUP BY user_id;
5. 多个聚合函数一起使用
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount,
MAX(amount) AS max_amount,
MIN(amount) AS min_amount
FROM orders
GROUP BY user_id;
这类写法在报表统计中非常常见。
WHERE 和 HAVING 的区别
这是 GROUP BY 最容易混淆的地方之一。
WHERE:分组前过滤
比如只统计已支付订单:
SELECT user_id, COUNT(*) AS total_orders
FROM orders
WHERE status = 1
GROUP BY user_id;
这里先把 status = 1 的数据筛出来,再进行分组。
HAVING:分组后过滤
比如只保留订单数大于 1 的用户:
SELECT user_id, COUNT(*) AS total_orders
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 1;
这里先分组,再对分组结果进行筛选。
一起使用的场景
SELECT user_id, SUM(amount) AS total_amount
FROM orders
WHERE status = 1
GROUP BY user_id
HAVING SUM(amount) > 200;
含义是:
- 先筛选已支付订单
- 再按用户分组
- 最后只保留消费总额大于 200 的用户
一句话区分
WHERE过滤的是原始记录HAVING过滤的是分组结果
GROUP BY 和 ORDER BY 结合使用
分组查询后,通常还需要排序。
按用户订单总金额倒序排列
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
ORDER BY total_amount DESC;
这里 ORDER BY 排的是聚合后的结果。
也可以直接写聚合表达式:
SELECT user_id, SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
ORDER BY SUM(amount) DESC;
通常使用别名更清晰。
GROUP BY 与日期统计
业务里经常需要按天、按月、按年做统计。
按天统计订单数量
SELECT DATE(create_time) AS order_date, COUNT(*) AS total_orders
FROM orders
GROUP BY DATE(create_time)
ORDER BY order_date ASC;
按月统计订单总金额
SELECT DATE_FORMAT(create_time, '%Y-%m') AS order_month, SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(create_time, '%Y-%m')
ORDER BY order_month ASC;
这种写法很常见,但要注意一个问题:对字段使用函数可能影响索引利用率。如果数据量大,统计类查询要结合实际场景优化。
GROUP BY 查询时为什么有时报错
最常见的报错和这条 SQL 有关:
SELECT user_id, product_name, COUNT(*)
FROM orders
GROUP BY user_id;
很多人会问:为什么这样写有时能执行,有时会报错?
原因在于:product_name 既没有出现在 GROUP BY 中,也没有被聚合函数包裹。
为什么这是个问题
按 user_id 分组后,一个用户可能对应多条订单、多个 product_name。 那最终结果里 product_name 应该显示哪一个?
从逻辑上说,这是不确定的。
MySQL 5.7+ 中常见现象
如果开启了 ONLY_FULL_GROUP_BY,这类 SQL 会直接报错。因为它不符合标准 SQL 语义。
正确写法通常有三种:
写法一:把字段加入分组条件
SELECT user_id, product_name, COUNT(*)
FROM orders
GROUP BY user_id, product_name;
写法二:对字段使用聚合函数
SELECT user_id, MAX(product_name) AS product_name, COUNT(*) AS total
FROM orders
GROUP BY user_id;
写法三:明确知道业务语义时使用 ANY_VALUE()
SELECT user_id, ANY_VALUE(product_name) AS product_name, COUNT(*) AS total
FROM orders
GROUP BY user_id;
但要注意,ANY_VALUE() 只是告诉 MySQL:这个字段取组内任意一个值即可。 它不会保证业务语义一定正确,所以不能滥用。
ONLY_FULL_GROUP_BY 到底是什么
它是 MySQL 的一种 SQL 模式,用来限制不规范的分组查询。
开启后,SELECT 中出现的非聚合字段,必须满足以下条件之一:
- 出现在
GROUP BY中 - 被聚合函数包裹
- 能被 MySQL 判断为函数依赖字段
例如主键依赖场景下,某些查询是允许的。但在日常开发里,不要依赖这种边界行为,最稳妥的方式还是写出语义明确的 SQL。
GROUP BY 的典型业务场景
按部门统计员工人数
CREATE TABLE employee (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(50) NOT NULL,
dept_name VARCHAR(50) NOT NULL,
salary DECIMAL(10,2) NOT NULL,
hire_date DATE NOT NULL
);
SELECT dept_name, COUNT(*) AS emp_count
FROM employee
GROUP BY dept_name;
按部门统计平均薪资
SELECT dept_name, AVG(salary) AS avg_salary
FROM employee
GROUP BY dept_name;
按商品统计销量
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
product_name VARCHAR(100) NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL
);
SELECT product_id, product_name, SUM(quantity) AS total_quantity
FROM order_item
GROUP BY product_id, product_name
ORDER BY total_quantity DESC;
查找重复数据
GROUP BY 还有一个高频用途:查重。
例如查找重复手机号:
CREATE TABLE user_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(100) DEFAULT NULL
);
SELECT phone, COUNT(*) AS repeat_count
FROM user_info
GROUP BY phone
HAVING COUNT(*) > 1;
这类 SQL 很适合数据治理和清洗场景。
GROUP BY 和 DISTINCT 的区别
很多人刚接触时会把 GROUP BY 和 DISTINCT 混在一起,但它们用途并不相同。
DISTINCT:去重
SELECT DISTINCT status
FROM orders;
作用是去掉重复值,返回唯一的 status。
GROUP BY:分组汇总
SELECT status, COUNT(*) AS total
FROM orders
GROUP BY status;
作用是按 status 分组后,再做统计。
什么时候可以互相替代
在某些简单场景下:
SELECT DISTINCT status FROM orders;
和
SELECT status FROM orders GROUP BY status;
结果可能一样。
但语义上:
DISTINCT强调去重GROUP BY强调分组统计
在需要聚合计算时,应该优先想到 GROUP BY。
GROUP BY 的性能注意事项
GROUP BY 本质上不是“慢”,但在数据量大时,确实容易成为性能瓶颈。
1. 分组字段最好有合适索引
例如:
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id;
如果 user_id 上有索引,执行效率通常更高。
CREATE INDEX idx_user_id ON orders(user_id);
2. 避免对索引字段做函数运算后再分组
例如:
SELECT DATE(create_time), COUNT(*)
FROM orders
GROUP BY DATE(create_time);
这类写法可读性高,但可能导致索引无法被高效利用。
如果查询量很大,可以考虑:
- 通过范围条件先过滤
- 增加冗余统计字段
- 做离线汇总表
- 使用物化统计方案
3. 先过滤再分组
能放在 WHERE 的条件,不要放到 HAVING。
例如:
不推荐:
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING user_id > 100;
更推荐:
SELECT user_id, COUNT(*)
FROM orders
WHERE user_id > 100
GROUP BY user_id;
因为 WHERE 会在分组前减少参与计算的数据量。
4. 分组字段不要过多、过杂
分组维度越多,分组结果越细,开销通常越大。
例如:
GROUP BY user_id, status, product_name, DATE(create_time)
这类语句未必有问题,但要确认是否真的需要这么细的统计粒度。
5. 明细查询和统计查询分离
很多系统把列表页查询和报表统计混在一起,导致 SQL 又长又慢。 对于高并发业务,通常建议:
- 明细查询单独写
- 聚合统计单独写
- 重报表做异步汇总或缓存
GROUP BY 的几个高频误区
误区一:GROUP BY 只是去重
不是。 它虽然能表现出类似去重的效果,但本质是分组,核心价值在于配合聚合函数做汇总分析。
误区二:分组后可以随便查任意字段
不行。 没有参与分组、也没有聚合的字段,在严格 SQL 模式下通常不允许直接出现在 SELECT 中。
误区三:HAVING 可以替代 WHERE
不推荐。 虽然某些查询能写出来,但语义和性能都不如 WHERE 合适。
误区四:COUNT(字段) 和 COUNT(*) 完全一样
不完全一样。
COUNT(*):统计记录数COUNT(column):统计该列不为NULL的数量
例如:
SELECT COUNT(*) AS total_rows, COUNT(email) AS non_null_email
FROM user_info;
如果 email 有空值,这两个结果就不同。
实战示例:一条完整的分组统计 SQL
需求:统计每个用户已支付订单的数量、总金额、平均金额,并按总金额倒序排列,只保留总金额大于 200 的用户。
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(amount) AS total_amount,
AVG(amount) AS avg_amount
FROM orders
WHERE status = 1
GROUP BY user_id
HAVING SUM(amount) > 200
ORDER BY total_amount DESC;
这条 SQL 基本覆盖了 GROUP BY 的核心用法:
WHERE:分组前过滤GROUP BY:按用户分组- 聚合函数:统计数量、总金额、平均值
HAVING:过滤分组结果ORDER BY:对统计结果排序
能真正理解这条 SQL,GROUP BY 的核心就算掌握了。
开发中如何写出更稳妥的 GROUP BY
可以遵循这几个原则:
- 先想清楚“按什么维度统计”
- 再决定“每组要算什么指标”
- 非分组字段不要直接放进
SELECT - 尽量把过滤条件放进
WHERE - 对大表统计,优先考虑索引和汇总方案
- 在 MySQL 5.7+ 环境下,注意
ONLY_FULL_GROUP_BY对 SQL 合法性的影响
总结
GROUP BY 的本质是:把多行数据按指定字段归并成多个组,再对每个组进行统计计算。
掌握它时,最关键的不是死记语法,而是理解这几个问题:
- 分组维度是什么
- 每个组要统计什么
- 哪些条件属于分组前过滤,哪些属于分组后过滤
- 哪些字段可以出现在
SELECT中 - 大数据量下如何控制性能
只要这几个点清楚了,GROUP BY 就不只是“会用”,而是真正能在业务查询、报表统计、数据分析和排查问题中用得稳、用得准。