GROUP BY 是什么
GROUP BY 按一列或多列把行归并成组,组内用聚合函数计算,是统计类查询的基础。MySQL 8.0 默认开启 ONLY_FULL_GROUP_BY:SELECT 列表里的非聚合列必须出现在分组列中,否则直接报错。分组通常需要临时表或排序来支撑,分组键上有合适索引、尤其是与 WHERE 条件口径对齐时,开销会明显下降。过滤放在分组前用 WHERE,放在分组后用 HAVING,两者代价差别很大。
基本信息
| 用途 | 分组统计与汇总 |
|---|---|
| 语法 | GROUP BY 列1, 列2 [WITH ROLLUP] [HAVING 条件] |
| 默认约束 | ONLY_FULL_GROUP_BY,5.7 与 8.0 均默认开启 |
| 过滤时机 | WHERE 在分组前,HAVING 在分组后 |
| 常用聚合 | COUNT / SUM / AVG / MIN / MAX / GROUP_CONCAT |
| 索引影响 | 分组键与过滤条件对齐时可用索引,否则临时表加排序 |
典型使用场景
- 按状态统计订单量与金额,作为业务看板的基础数据
- 按天或按月汇总趋势,用日期列或日期函数分组
- 多维度交叉统计:日期与状态、渠道与地区组合
- 找出重复数据:按业务键分组后筛出计数大于一的组
- 统计每个用户的订单数并倒序,定位活跃用户与异常账号
- 用 WITH ROLLUP 生成带小计与合计的报表,减少后续加工
基础分组
基础分组
SELECT status, COUNT(*) AS cnt, SUM(amount) AS total # 分组列加聚合列
FROM orders
WHERE created_at >= '2026-10-01' # 先过滤再分组,代价最低
GROUP BY status
HAVING total > 1000 # 分组后再过滤,可引用聚合别名
ORDER BY total DESC; # 按统计值排序多列分组与小计
多列分组与小计
SELECT DATE(created_at) AS d, status, COUNT(*) AS cnt
FROM orders
GROUP BY d, status WITH ROLLUP # 追加小计与总计行
ORDER BY d DESC, status; # 小计行的分组列为空,取值时要区分
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id HAVING cnt > 1 # 查重:分组后过滤
ORDER BY cnt DESC LIMIT 20;常见写法与代价
| 写法 | 含义 | 注意点 |
|---|---|---|
| GROUP BY 列 | 按列归并分组 | 非聚合列必须出现在分组列中,否则报错 |
| WHERE ... GROUP BY | 先过滤再分组 | 过滤越早,参与分组的行越少 |
| HAVING | 对分组结果过滤 | 能用 WHERE 表达的不要挪到 HAVING |
| GROUP BY 表达式 | 按计算结果分组 | 无法用索引,通常伴随临时表 |
| WITH ROLLUP | 追加小计与总计行 | 小计行的分组列为空,读取时须区分 |
| DISTINCT | 去重 | 不做聚合时语义更清晰,优先选它 |
| ORDER BY 聚合结果 | 按统计值排序 | 可引用别名,排序发生在分组之后 |
能用 WHERE 过滤的条件不要放进 HAVING:WHERE 在分组前剔除行,参与分组的行更少、临时表更小;HAVING 只能过滤算完的分组结果,等于把代价留到了最后。
分组列为 NULL 的行会被归成同一组,统计结果里会多出一个分组列为空的组;报表里要显式处理这类记录,否则容易被误当成脏数据。
关闭 ONLY_FULL_GROUP_BY 只是让有歧义的语句不再报错,非聚合列取到的是组内任意一行的值,结果不确定;正确做法是补齐分组列或用聚合函数包裹。
分组写法检查清单
- 分组键尽量与过滤条件对齐,例如先按状态过滤再按状态分组
- 索引建在分组键与过滤列上,聚合列本身不需要索引
- 统计口径先对齐:不同计数写法对空值处理不同,混用会让数字对不上
- 需要精确去重计数时用去重聚合,但大表开销高,可评估近似方案
- 小计与总计交给分组小计语法或单独查询,别用程序逐行累加
- 分组列上存在大量空值时先确认业务含义,再决定是否归并
统计所有行、统计某列非空的行、去重计数是三种不同口径:前者不忽略空值,中间那种会跳过该列为空的行,去重计数还要额外排序去重,同一报表里混用会让汇总数字对不上。