# GROUP BY：把一行行订单压成一张统计表，HAVING 该站在哪一步

聚合函数怎么把多行算成一个数、GROUP BY 怎么分组，以及 WHERE 和 HAVING 分别在分组前后筛什么

> GROUP BY · HAVING · 聚合函数 · 约 7 分钟 · 10 月 04 日

## 本篇要点

1. 聚合函数把一个组里的很多值压成一个值：COUNT 数个数、SUM 求和、AVG 求平均、MAX/MIN 取极值。
2. GROUP BY 先按某列的值把行分成若干组，再让每个组各算一次聚合、各输出一行，所以结果行数等于组的个数。
3. SELECT 里的每一列，要么出现在 GROUP BY 里，要么包在聚合函数里；否则分组后每组只剩一行、裸列有多个候选值，MySQL 5.7.5 起默认会报 ERROR 1055。
4. 逻辑执行顺序是 FROM → WHERE → GROUP BY → 聚合 → HAVING → SELECT → ORDER BY，所以 WHERE 里不能写聚合函数，也不能用 SELECT 的别名。
5. WHERE 筛行、发生在分组之前，被筛掉的行不参与任何统计；HAVING 筛组、发生在聚合之后，组里的每一行都已经算进去了。
6. WHERE 和 HAVING 可以同时使用，且把条件尽量早放在 WHERE 能减少后续要处理的数据量。
7. GROUP BY 把 NULL 当成一个值，所有该列为空的行归入同一组；COUNT(*) 数行，COUNT(col) 只数非空的格。
8. 不写 ORDER BY 时组的顺序不保证，需要固定顺序要显式排序。

---

前两篇里，你写出的每条查询都是“一行进、一行出”：`WHERE` 只是在决定哪些行留下，行的条数变了，每一行的形状没变。可现实里的取数问题经常是另一类——“每个城市一共多少单、总共多少钱”。这类问题的答案不是某几行订单，而是每个城市一个数。要把 8 行订单变成 3 行城市统计，需要两件事配合：聚合函数负责把一堆值压成一个数，`GROUP BY` 负责决定“按什么把它们分成堆”。

分完组之后还会冒出一个新问题：有些条件筛的是一行一行的订单，有些条件筛的是一个一个的组，两者不能放在同一个位置。这就是 `HAVING` 出场的地方。

## 聚合函数：把一堆值压成一个数

聚合函数的作用，就是把一个“堆”里的很多值算成一个值。最常用的几个：`COUNT()` 数个数，`SUM()` 求和，`AVG()` 求平均，`MAX()`、`MIN()` 取最大和最小值。

```sql
SELECT COUNT(*) AS order_count,
       SUM(amount) AS total_amount
FROM orders;
```

这条查询没有 `GROUP BY`，含义是“整张 `orders` 表算一个堆”：一共 8 行，金额合计 949.90。

这里顺带认识了 `AS`：它给结果里的这一列起个名字，表头显示成 `order_count`、`total_amount`，比 `COUNT(*)` 好读，后面也用得上。

## GROUP BY：按某一列分成几堆，每堆出一行

现在要求“每个城市的下单情况”。整张表当一个堆显然不够，得先按城市分开：

```sql
SELECT city,
       COUNT(*) AS order_count,
       SUM(amount) AS total_amount
FROM orders
GROUP BY city;
```

`GROUP BY city` 让数据库逐行去看 `city` 这一格的值，值相同的行归到同一堆。这一篇里 `orders` 又多了三行（1006 到 1008）：

| order_id | user_id | city | amount | created_at |
|---|---|---|---|---|
| 1001 | 8823 | 杭州 | 129.00 | 2024-03-01 10:23:11 |
| 1002 | 9107 | 上海 | 58.50 | 2024-03-01 11:07:42 |
| 1003 | 8823 | 杭州 | 320.00 | 2024-03-02 09:15:03 |
| 1004 | 9310 | NULL | 76.00 | 2024-03-02 14:02:50 |
| 1005 | 9107 | 杭州 | 45.00 | 2024-03-03 08:40:12 |
| 1006 | 9107 | 上海 | 210.00 | 2024-03-03 19:22:05 |
| 1007 | 9310 | 上海 | 91.50 | 2024-03-04 12:05:33 |
| 1008 | 8823 | NULL | 19.90 | 2024-03-04 21:41:07 |

8 行按 `city` 分成三堆：杭州 3 行（1001、1003、1005），上海 3 行（1002、1006、1007），城市为空的那 2 行（1004、1008）归在一起。然后每个堆各算一次聚合，最后每堆输出一行：

| city | order_count | total_amount |
|---|---|---|
| 杭州 | 3 | 494.00 |
| 上海 | 3 | 360.00 |
| NULL | 2 | 95.90 |

杭州那一行就是 129 + 320 + 45 = 494.00。**输入 8 行，输出 3 行——行数等于组的个数，这是 `GROUP BY` 最本质的变化。**

不写 `ORDER BY` 时，这三行的先后顺序是不保证的：数据库觉得怎么方便就怎么给，下次跑可能就换了顺序。要固定顺序，就在语句最后加一句排序，比如按总额从高到低：

```sql
ORDER BY total_amount DESC
```

`DESC` 是从大到小，不写则默认从小到大。`ORDER BY` 可以写在 `SELECT` 里起的别名，所以这里直接写 `total_amount`。

## SELECT 里哪些列能写：要么分组，要么聚合

分组之后每个组只输出一行，所以组里只有分组列还剩唯一确定的值。看这条：

```sql
SELECT city, order_id, SUM(amount) AS total_amount
FROM orders
GROUP BY city;
```

杭州这一组里有 1001、1003、1005 三个 `order_id`，让数据库填哪一个都不对——它没有依据判断你想看哪个。MySQL 5.7.5 及以后版本默认开启 `ONLY_FULL_GROUP_BY`，遇到这种情况会直接报错：

```
ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY
clause and contains nonaggregated column ... which is incompatible
with sql_mode=only_full_group_by
```

在更老的 MySQL 版本，或者有人手动关掉这个设置之后，这句不会报错，但数据库只能从组里随便挑一个 `order_id` 给你，挑到哪个是不确定的，加 `ORDER BY` 也影响不了它 [1]。所以看到 1055 这类报错，正确做法是改查询，而不是去关设置。

规则记成一句话就是：**`SELECT` 里的每一列，要么出现在 `GROUP BY` 里，要么包在聚合函数里。**两种都不满足，就是让数据库在一组里做无缘无故的选择。

## WHERE 和 HAVING：同一个条件，位置不同，筛的东西也不同

现在想“只保留下了 3 单及以上的城市”。很自然会想接着写 `WHERE COUNT(*) >= 3`，但这条会报错：

```sql
SELECT city, COUNT(*) AS order_count
FROM orders
WHERE COUNT(*) >= 3
GROUP BY city;
```

原因在于 SQL 各子句的逻辑执行顺序，跟你写它们的顺序并不一样：

<figure>

```mermaid
flowchart LR
  F["FROM orders：拿到 8 行"] --> W["WHERE：一行一行筛"] --> G["GROUP BY city：分成 3 组"] --> A["聚合：每组算一个数"] --> H["HAVING：一组一组筛"] --> S["SELECT：每列算出来"] --> O["ORDER BY：调整行的顺序"]
```

</figure>

`WHERE` 排在聚合之前，它面对的还是一行一行的订单。在那个时刻，“这一组的 `COUNT(*)` 是多少”根本还不存在，所以 `WHERE` 里放聚合函数会直接报错。同理，`WHERE` 里也不能用 `SELECT` 起的别名，别名是更后面才出现的东西。

按组筛选要交给 `HAVING`，它排在聚合之后：

```sql
SELECT city, COUNT(*) AS order_count
FROM orders
GROUP BY city
HAVING COUNT(*) >= 3;
```

结果是杭州 3、上海 3，城市为空那一组因为只有 2 单被丢掉。这里的 `HAVING` 看的不是某一行，而是整个组算出来的那个数。

## 同一个业务问题，条件放哪儿，答案真的不一样

“只看金额超过 100 的订单，按城市汇总”和“按城市汇总，只看总额超过 100 的城市”，听起来接近，要的是两张表。第一句：

```sql
SELECT city, COUNT(*) AS order_count, SUM(amount) AS total_amount
FROM orders
WHERE amount > 100
GROUP BY city;
```

杭州剩 1001、1003，共 2 单、449.00；上海只剩 1006，共 1 单、210.00；城市为空的两单都不足 100 元，整组消失。

第二句：

```sql
SELECT city, SUM(amount) AS total_amount
FROM orders
GROUP BY city
HAVING SUM(amount) > 100;
```

杭州是 494.00，上海是 360.00。两个查询里杭州都留下了，但数字不同：`WHERE` 版的 449.00 是“大额订单加起来”，`HAVING` 版的 494.00 是“所有订单加起来，只是这家够大所以没被踢掉”。**`WHERE` 削掉的是行，被削掉的行从头到尾不参与统计；`HAVING` 削掉的是组，组里的每一行都已经算进去了。**这就是判断条件该放哪里的依据。

两者当然可以同时出现：`WHERE` 先把不看的订单删掉，`GROUP BY` 分组聚合，`HAVING` 再把不够格的城市删掉。写成这个顺序还有个附带好处：`WHERE` 越早筛掉不用的行，后面要分组和计算的数据就越少。

## 两件顺带要说清楚的事

**`NULL` 在分组里会被当成一个值。**所有 `city` 为空的行归到了同一组，这就是结果里出现一行 `NULL` 的原因——SQL 在分组时把 `NULL` 视为彼此相等 [2]。但要和 `COUNT` 的两种写法区分开：`COUNT(*)` 数的是组里有多少行，`COUNT(city)` 只数 `city` 这一格不是空的行。整张表 `COUNT(*)` 是 8，`COUNT(city)` 只有 6（1004 和 1008 的城市是空的）。想把城市为空的订单排除在统计之外，就在 `WHERE` 里写 `city IS NOT NULL`。

**没有 `GROUP BY` 时，整张表就是一个组。**所以 `SELECT COUNT(*), SUM(amount) FROM orders;` 只返回一行；这种情况下 `SELECT` 里更不能出现裸列，因为那个唯一的值同样是无从选择的。

把这一篇的规则收成一句：**行的问题交给 `WHERE`，组的问题交给 `HAVING`，中间夹着 `GROUP BY` 把行分成堆。**

<details>
<summary>练一下：每个用户各下了几单、总金额多少，只保留下单 2 单以上的用户，并按总金额从高到低排。</summary>

```sql
SELECT user_id,
       COUNT(*) AS order_count,
       SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2
ORDER BY total_amount DESC;
```

结果是 8823（3 单、468.90）、9107（3 单、313.50）、9310（2 单、167.50）。这里三个用户都下了 2 单以上，所以 `HAVING` 一行都没筛掉——它的作用是保证以后数据变了、或者换一张表跑时，答案仍然符合你要的口径。

</details>

## 术语表

- 聚合函数：把一个组里的很多个值算成一个值的函数，常用的有 COUNT、SUM、AVG、MAX、MIN。
- GROUP BY：按指定列的值把行分成若干组，每组各算一次聚合，最终每组输出一行。
- HAVING：写在 GROUP BY 之后，用整组的条件筛掉不符合的组，是能使用聚合函数的筛选位置。
- 逻辑执行顺序：FROM → WHERE → GROUP BY → 聚合 → HAVING → SELECT → ORDER BY，决定了每个子句能用到哪些东西。
- ONLY_FULL_GROUP_BY：MySQL 5.7.5 起的默认设置，禁止 SELECT 里出现既没分组也没聚合的裸列，触发时直接报错。
- AS 别名：给结果列临时起名字，方便看表头和排序引用。

## 来源

1. [MySQL 8.0 手册：ONLY_FULL_GROUP_BY 默认开启、ERROR 1055，以及关闭该模式后 MySQL 会从组里任意取值](https://dev.mysql.com/doc/refman/8.0/en/group-by-handling.html)
2. [SQLite SELECT 文档：分组时 NULL 被视为相等，WHERE 在分组之前过滤行](https://www.sqlite.org/lang_select.html)
3. [MySQL 手册 SELECT 语句：WHERE 不能用聚合函数，HAVING 在分组之后起作用](https://dev.mysql.com/doc/refman/8.0/en/select.html)
4. [True Order of SQL Operations：FROM、WHERE、GROUP BY、聚合、HAVING、SELECT 的逻辑先后关系](https://blog.jooq.org/a-beginners-guide-to-the-true-order-of-sql-operations/)

---

原文：https://pangzhengboyin.com/articles/sql-group-by-having-vs-where-1d3edc7b

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
