如何使用 SQL GROUP BY 进行分组统计?

陆遥
陆遥
发布时间:2026-08-23| 155人浏览过
原创

前言

很多人会写 GROUP BY,但真正容易出错的地方往往是结果语义:哪些列能放进 SELECT、聚合条件该写在 WHERE 还是 HAVING、多字段分组后的结果到底代表什么。本文围绕这几类最常见的误区逐项拆解,顺带补上纯去重、NULL 与空字符串等实战里经常被忽略的判断点。

写 GROUP BY 时,很多问题都不是“不会写”,而是结果语义和执行顺序容易混在一起:为什么有些库直接报错、为什么聚合条件不能放在 WHERE、多字段分组后结果为什么突然变多。下面就按这些高频误区拆开讲,并给出能直接套用的判断方式。

为什么 GROUP BY 后会报列必须出现在分组子句中

这类报错的根本原因是:SELECT 里出现了既没有参与聚合、也没有写进 GROUP BY 的列。这样一来,数据库在每个分组里无法确定该返回这列的哪一个值,因此会按 SQL 标准直接拒绝执行。

GROUP BY 报错原因与修正方式示意图
GROUP BY 报错怎么判断用结构化对比把“未分组列为何报错”以及两种常见修正方式讲清楚。

PostgreSQL 对这种写法一直比较严格,MySQL 5.7+ 在开启默认的 sql_mode=ONLY_FULL_GROUP_BY 后也会触发同类报错。

SELECT user_id, name, COUNT(*)
FROM orders
GROUP BY user_id;

上面这条语句的问题在于:name 既没有被聚合函数包裹,也没有出现在 GROUP BY user_id 中。只按 user_id 分组后,一组里可能对应多条订单记录,数据库无法判断该取哪一行的 name。

这类报错怎么改

常见修正方式有两种,取决于你的业务含义:

WHERE、GROUP BY、HAVING 的执行顺序信息图
WHERE 与 HAVING 执行位置把行过滤、分组统计和分组筛选放进同一条流程里,能直观看出为什么聚合条件不能写到 WHERE。
  • 如果 user_id 和 name 一一对应,可以把 name 也加入 GROUP BY。
  • 如果只是想从组里取一个值,就需要显式使用聚合函数,例如 MAX(name),或在 MySQL 中使用 ANY_VALUE(name)。

在 PostgreSQL 里,如果你的目标其实是“每个用户取一条代表性记录”,很多场景下会更适合用 DISTINCT ON,而不是做这种语义模糊的聚合。

WHERE 和 HAVING 到底该怎么分工

WHERE 和 HAVING 的区别,可以直接按一句话记:WHERE 过滤原始行,HAVING 过滤分组结果。

这不只是写法差异,更关系到 SQL 的执行顺序。先筛行,再分组,最后再判断哪些组需要保留。因此,聚合条件不能提前写进 WHERE。

什么时候必须用 HAVING

例如要查询“订单数超过 5 的用户”,正确条件必须写在 HAVING 里:

SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 5;

如果把 COUNT(*) > 5 写进 WHERE,大多数数据库都会直接报错,因为 WHERE 阶段聚合结果还没有产生。

先筛数据,再筛分组

如果问题变成“查询 2024 年订单中,订单数超过 5 的用户”,那就要把条件拆到两个阶段:

SELECT user_id, COUNT(*)
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY user_id
HAVING COUNT(*) > 5;

这里的 WHERE created_at >= '2024-01-01' 先把无关订单排除掉,之后 GROUP BY 统计的就是 2024 年范围内的数据,最后再用 HAVING COUNT(*) > 5 保留满足条件的分组。

性能上为什么别把条件都塞进 HAVING

从执行成本看,越早用 WHERE 过滤掉无关行,后面的 GROUP BY 要处理的数据就越少。把本该写在 WHERE 的条件全部推迟到 HAVING,通常只会让分组阶段承担更多不必要的工作。

多字段 GROUP BY 的结果应该怎么理解

多字段分组最容易误解的地方在于,它不是“分别按每个字段分一次组”,而是按字段组合后的唯一值分组。

多字段分组与 DISTINCT 适用场景对比图
组合分组与去重选择这一图把组合分组、NULL 归组、排序独立,以及纯去重优先 DISTINCT 四个点放在一起。

例如:

GROUP BY status, region

这表示会针对每个唯一的 (status, region) 组合生成一行结果。

为什么结果会变多

如果某个 status 同时存在于多个 region,它就会被拆成多行;反过来,同一个 region 出现在不同 status 下,也会形成多组。这是组合分组的正常表现,不是重复数据。

NULL 在 GROUP BY 里怎么处理

这里还有一个经常被忽略的细节:虽然 SQL 表达式里 NULL = NULL 的结果不是 true,但在 GROUP BY 中,所有 NULL 会被归到同一组。这属于标准行为。

排序和分组不是一回事

如果你想让结果按“地区优先,再按状态”显示,需要另外写排序条件:

ORDER BY region, status

ORDER BY 和 GROUP BY 解决的是不同问题,二者顺序也没有绑定关系。分组决定如何聚合,排序决定结果怎么展示。

只想去重时,GROUP BY 和 DISTINCT 该选谁

用 GROUP BY 去重在语法上可行,但如果唯一目的只是去重,通常不推荐这么写。

SELECT user_id FROM logs GROUP BY user_id;

这条 SQL 的确能得到不重复的 user_id,但它本质上还是在走分组逻辑。相同意图下,下面的写法语义更直接:

SELECT DISTINCT user_id FROM logs;

为什么一般优先用 DISTINCT

  • GROUP BY 是为聚合统计设计的,DISTINCT 更符合“只去重”的表达意图。
  • 多数数据库对 DISTINCT 有专门优化,可读性和维护性也通常更好。
  • 如果你还要顺手统计每个值出现的次数,那才自然切换为 GROUP BY user_id 加 COUNT(*)。

什么时候才考虑用 GROUP BY 替代 DISTINCT

只有在一些例外环境里,比如旧版 SQLite 或某些嵌入式数据库对多列 DISTINCT 支持有限时,才有理由把 GROUP BY 当成替代方案。这不是常规写法,更不应该当成通用习惯。

别忽略 NULL 和空字符串的分组差异

实际业务里,一个很容易踩坑的点是:空字符串 '' 和 NULL 在 GROUP BY 里永远不是同一类值。哪怕业务含义上它们都表示“未填写”,数据库也会把它们拆成两组。

这意味着,分组前如果不先统一清洗数据,就可能得到和业务认知不一致的统计结果。遇到这类字段时,应该先明确“未填写”的统一表示方式,再做聚合统计。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。