首页/数据/SQL 速查/GROUP BY 分组统计
聚合与窗口函数54 条

GROUP BY 分组统计

按列值分组后对每组计算聚合:SELECT 列表里只能出现分组列与聚合表达式,这是标准与两库默认强制的约束。

SQL 速查数据更新于 2026-10-13

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 仍要显式列出分组键。
分组键数量影响结果粒度:多分一列就多一层细分,统计前先想清楚每行代表什么,避免维度重复导致总量翻倍。

易错点

  • SELECT 里出现非分组列:严格模式报错,宽松模式取到的是该组里不确定的某一行
  • 把过滤条件写在 HAVING 而本该写 WHERE:先聚合后过滤,扫描量更大且语义可能不同
  • 分组粒度选错会放大或缩小结果,例如按订单明细分组后统计订单数会重复计订单
  • GROUP BY 里用函数包住列(如 DATE(created_at))无法利用普通索引,大数据量下退化为全表分组
  • COUNT(col) 与 COUNT(*) 混用导致口径与预期不一致,统计前先确认是否要把 NULL 算进去

数据来源与参考

  • MySQL 8.0 Reference Manual · GROUP BY
  • PostgreSQL Documentation · GROUP BY
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌