GROUP BY 是什么
GROUP BY 把行按一个或多个列的值合并成组,再对每组计算聚合函数,输出行数等于组的个数。标准 SQL 要求 SELECT 列表里只能出现分组列与聚合表达式,MySQL 8.0 默认开启 ONLY_FULL_GROUP_BY 后同样强制这一点。分组发生在 WHERE 之后、HAVING 之前,所以行级过滤只能写 WHERE,组级过滤只能写 HAVING。分组时多个 NULL 会被当作同一个值,归入同一组。
基本信息
| 作用 | 按列值合并行,配合聚合函数产出统计结果 |
|---|---|
| 书写位置 | FROM / WHERE 之后,HAVING / ORDER BY 之前 |
| SELECT 限制 | 只能出现分组列与聚合表达式,严格模式强制 |
| NULL 分组 | 多个 NULL 视为同一个值,归入同一组 |
| 多列分组 | GROUP BY a, b 决定组粒度,组合越多组越细 |
| 输出顺序 | 不保证,需要稳定顺序必须显式 ORDER BY |
典型使用场景
- 时间维度报表:GROUP BY DATE(created_at) 统计每日订单量
- 按维度汇总:按渠道、地区、商品类目统计销售额
- 人均与频次:GROUP BY user_id 后 COUNT 得到每个用户的下单次数
- 聚合后再筛:HAVING COUNT(*) > 5 找出下单超过五次的用户
- 数据质量检查:GROUP BY 期望唯一的列后 HAVING COUNT(*) > 1 找重复记录
- 交叉统计:CASE 表达式配合 GROUP BY 把行转成不同口径的列
示例
示例
SELECT u.city,
COUNT(*) AS user_cnt, # 每城市用户数
ROUND(AVG(o.amount), 1) AS avg_amount, # 人均消费
SUM(o.amount) AS total_amount # 消费总额
FROM users AS u
JOIN orders AS o ON o.user_id = u.id
WHERE u.status = 1 # 先过滤行,此时不能用聚合
GROUP BY u.city # 再分组
HAVING COUNT(*) >= 100 # 再过滤组
ORDER BY user_cnt DESC; # 最后排序严格模式的报错与修正
严格模式的报错与修正
-- MySQL 8.0 默认 ONLY_FULL_GROUP_BY,下句会报 ERROR 1055
SELECT city, name, COUNT(*) FROM users GROUP BY city;
-- 修正一:把非分组列包进聚合函数
SELECT city, MIN(name) AS sample_name, COUNT(*) FROM users GROUP BY city;
-- 修正二:把该列补进分组键,组粒度随之变细
SELECT city, name, COUNT(*) FROM users GROUP BY city, name;常用聚合函数
| 函数 | 含义 | NULL 处理 |
|---|---|---|
| COUNT(*) | 统计行数 | 含 NULL 行 |
| COUNT(col) | 统计该列非空值个数 | 忽略 NULL |
| SUM(col) | 求和 | 忽略 NULL,全为 NULL 时返回 NULL |
| AVG(col) | 平均值 | 分母是非 NULL 行数 |
| MIN / MAX | 最小与最大值 | 忽略 NULL |
| GROUP_CONCAT / string_agg | 组内拼接字符串 | MySQL 与 PostgreSQL 函数名不同 |
GROUP BY 之后输出行的顺序不保证,需要稳定顺序必须显式 ORDER BY,尤其在分页与对账导出时。
COUNT(DISTINCT col) 才是去重计数;写成 COUNT(col) 不会去重,两者结果在存在重复值时不同。
ONLY_FULL_GROUP_BY 报错时不要用 ANY_VALUE() 或临时关闭严格模式掩盖,先确认分组口径是否本来就该更细。
分组键上建索引有利于去重与分组,但结果组数很多时仍可能落到临时表,用 EXPLAIN 关注 Extra 里的 Using temporary 与 Using filesort。
按天统计示例
按天统计示例
SELECT DATE(created_at) AS d, # 截断到天作为分组键
COUNT(*) AS order_cnt,
SUM(amount) AS amount
FROM orders
WHERE created_at >= '2024-05-01'
AND created_at < '2024-06-01' # 半开区间,边界不漏
GROUP BY DATE(created_at)
ORDER BY d;
# 注意:分组键被函数包住,created_at 上的索引只能用于 WHERE 范围过滤分组与去重的取舍
- 只要去重后的一组值:用 GROUP BY 或 DISTINCT 都可以,窄列场景下 DISTINCT 更直观
- 既要去重又要算每组统计量:必须用 GROUP BY,DISTINCT 表达不了聚合
- 要去重计数:COUNT(DISTINCT col) 一步到位,不必先分组再数行数
- 组内排序取前几名:GROUP BY 做不了,改用窗口函数 ROW_NUMBER 或 RANK
- 需要保留明细行:GROUP BY 会合并行,改用窗口函数在明细上算聚合
- 分组结果再关联其他表:把分组结果写成子查询或 CTE,外层再连接,避免嵌套过深
GROUP BY 与 DISTINCT 在只做去重时等价,很多优化器会把两者改写成同一种执行方式;选择依据是可读性与后续是否需要聚合。
PostgreSQL 允许 GROUP BY 主键后引用该表其它列(函数依赖推断),MySQL 的 ONLY_FULL_GROUP_BY 不认这种推导,跨库 SQL 仍要显式列出分组键。
分组键数量影响结果粒度:多分一列就多一层细分,统计前先想清楚每行代表什么,避免维度重复导致总量翻倍。