SQL 数据分析 · 指南 · 2026年10月29日 · 11 分钟阅读
GROUP BY 和 HAVING 的区别(以及 WHERE 和 HAVING 的区别),附实例

GROUP BY 和 HAVING 不是二选一的关系,它们在同一个查询里各干各的活。GROUP BY 把行合并成组(每个机场一行、每天一行、每个舱位一行),让 COUNT、AVG 这类聚合函数按组计算。HAVING 在聚合结果出来之后筛选这些组。WHERE 是第三个角色:它在分组之前筛选单独的行。所以最简短的规则是:针对原始列的条件写在 WHERE 里;针对聚合结果的条件(COUNT(*) > 1000、AVG(amount) > 300)写在 HAVING 里。
下面所有例子都在 Nomto SQL 课程使用的航空公司数据库上运行:2026 年夏季 104 个机场之间的 33,121 个航班,以及订单、机票和航段数据,金额单位为美元。每个查询都可以粘贴到 Nomto 的 SQL 练习场里自己核对。
GROUP BY 做什么
没有 GROUP BY 时,聚合函数会把整张表变成一行;有了它,分组列的每个不同取值得到一行。
SELECT status, COUNT(*) AS flights
FROM flights
GROUP BY status
ORDER BY flights DESC;
| status | flights |
|---|---|
| Arrived | 16707 |
| Scheduled | 15383 |
| On Time | 518 |
| Cancelled | 414 |
| Departed | 58 |
| Delayed | 41 |
六种状态,六行。每一行是一个组,COUNT(*) 在每个组内单独计算。
可以按多列分组,也可以按表达式分组。下面统计每种机型的座位数,同时在同一次扫描里用条件计数算出商务舱座位数:
SELECT aircraft_code,
COUNT(*) AS seats,
SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) AS business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
| aircraft_code | seats | business |
|---|---|---|
| 773 | 402 | 30 |
| 763 | 222 | 30 |
| 321 | 170 | 28 |
| 320 | 140 | 20 |
| 733 | 130 | 12 |
| 319 | 116 | 20 |
| SU9 | 97 | 12 |
| CR2 | 50 | 0 |
(第九种机型 CN1 有 12 个座位,没有商务舱。)
HAVING 做什么
HAVING 决定整组保留还是丢弃。它在聚合值算出来之后才执行,所以可以引用聚合函数。
SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE status = 'Arrived'
GROUP BY departure_airport
HAVING COUNT(*) > 1000
ORDER BY departures DESC;
| departure_airport | departures |
|---|---|
| DME | 1618 |
| SVO | 1503 |
按数据库的执行顺序来读:取出所有航班,只保留已到达的(WHERE),按出发机场分组,每组计数,只保留超过 1,000 行的组(HAVING)。莫斯科的两个机场,多莫杰多沃和谢列梅捷沃,通过了筛选。
HAVING 可以用任何聚合函数,不一定是 SELECT 里出现的那个。哪些机型完全没有商务舱?
SELECT aircraft_code, COUNT(*) AS seats
FROM seats
GROUP BY aircraft_code
HAVING SUM(CASE WHEN fare_conditions = 'Business' THEN 1 ELSE 0 END) = 0;
| aircraft_code | seats |
|---|---|
| CR2 | 50 |
| CN1 | 12 |
也就是 Bombardier CRJ-200 和 Cessna 208 Caravan。
WHERE 和 HAVING 的区别
| WHERE | HAVING | |
|---|---|---|
| 筛选对象 | 单独的行 | 组 |
| 执行时机 | GROUP BY 之前 |
GROUP BY 和聚合之后 |
| 能否使用聚合函数 | 不能 | 能 |
| 能否使用原始列 | 任何列 | 只能用分组列(或放在聚合函数里) |
没有 GROUP BY 时能否使用 |
能 | 能,整个结果视为一组 |
实际影响是:WHERE 条件会改变进入聚合计算的数据,HAVING 条件不会。 对比下面两个关于航段票价的查询。
各舱位的平均票价,只保留价格超过 300 美元的航段:
SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
WHERE amount > 300
GROUP BY fare_conditions
ORDER BY avg_price DESC;
| fare_conditions | segments | avg_price |
|---|---|---|
| Business | 63467 | 739.73 |
| Economy | 90837 | 500.99 |
| Comfort | 6919 | 474.92 |
各舱位的平均票价,只保留平均值超过 300 美元的舱位:
SELECT fare_conditions, COUNT(*) AS segments, ROUND(AVG(amount), 2) AS avg_price
FROM ticket_flights
GROUP BY fare_conditions
HAVING AVG(amount) > 300
ORDER BY avg_price DESC;
| fare_conditions | segments | avg_price |
|---|---|---|
| Business | 107642 | 511.43 |
| Comfort | 17291 | 327.41 |
第一个查询回答的是「各舱位里,贵的航段平均多少钱」;由于便宜的航段被扔掉了,每个平均值都被抬高,经济舱甚至超过了舒适舱。第二个查询回答的是「哪些舱位平均来说比较贵」;这里的平均值是真实的(经济舱是 159.60 美元,所以被筛掉)。同样的列、同样的数字 300,问的却是完全不同的问题。
两处都能写时,用 WHERE
针对分组列的条件,技术上写在哪里都行:
SELECT departure_airport, COUNT(*) AS departures
FROM flights
WHERE departure_airport IN ('SVO', 'DME', 'VKO')
GROUP BY departure_airport
ORDER BY departures DESC;
| departure_airport | departures |
|---|---|
| DME | 3217 |
| SVO | 2981 |
| VKO | 1719 |
把条件改成 HAVING departure_airport IN ('SVO', 'DME', 'VKO'),返回的也是这三行。但 HAVING 版本要先对全部 104 个机场分组计数,再扔掉其中 101 组。很多优化器会自动把这类条件下推,但你不该依赖这一点;而且 WHERE 也能向读代码的人表明你的意图:这些行从一开始就不在统计范围内。
SQL 的执行顺序
你先写的是 SELECT,但数据库按下面的逻辑顺序处理各个子句:
FROM和JOIN:构造行集合WHERE:丢弃行GROUP BY:形成分组- 按组计算聚合函数
HAVING:丢弃组SELECT:计算输出列和别名DISTINCTORDER BYLIMIT/OFFSET
几乎所有 GROUP BY / HAVING 的错误都能从这张清单推出来:
WHERE执行时聚合值还不存在,所以WHERE COUNT(*) > 1000不可能成立。SELECT在GROUP BY之后执行,所以每个被选出的列在每组里只能有一个值。ORDER BY在SELECT之后执行,所以可以用departures这样的列别名。- 在 PostgreSQL 里,
HAVING在SELECT之前执行,所以不能用SELECT里的别名:要写HAVING COUNT(*) > 1000,而不是HAVING departures > 1000。有些数据库(MySQL、DuckDB)接受别名,这也是在不同数据库之间复制代码时,这里偶尔会出错的原因。
常见错误
1. 在 WHERE 里用聚合函数
SELECT departure_airport, COUNT(*)
FROM flights
WHERE COUNT(*) > 1000
GROUP BY departure_airport;
这个查询会报错。PostgreSQL 的提示是 “aggregate functions are not allowed in WHERE”。把条件挪到 HAVING 里即可。
2. 选出的列既没分组也没聚合
SELECT departure_airport, arrival_airport, COUNT(*)
FROM flights
GROUP BY departure_airport;
这个也会报错:“column arrival_airport must appear in the GROUP BY clause or be used in an aggregate function”。一个出发机场对应很多个到达机场,数据库没法为每组挑出一个值。要么把 arrival_airport 加进 GROUP BY(每条航线一行),要么对它做聚合,比如 COUNT(DISTINCT arrival_airport)。
PostgreSQL 有一个例外:如果按某张表的主键分组,就可以选出这张表的其他列。按 b.book_ref 分组时可以直接选 b.total_amount。把两者都写进 GROUP BY 在所有数据库里都能用,所以这里的查询都这样写:
SELECT b.book_ref, COUNT(*) AS tickets, b.total_amount
FROM bookings b
JOIN tickets t ON t.book_ref = b.book_ref
GROUP BY b.book_ref, b.total_amount
HAVING COUNT(*) >= 5
ORDER BY b.total_amount DESC
LIMIT 5;
| book_ref | tickets | total_amount |
|---|---|---|
| 65A6EA | 5 | 10656.00 |
| 7734A5 | 5 | 8291.00 |
| 148C46 | 5 | 8135.00 |
| 070682 | 5 | 8012.00 |
| 8DA112 | 5 | 6150.00 |
整个数据库里只有 23 笔订单有五张或以上的机票。
3. 把小组和大组放在一起排名
按机场统计实际起飞航班的平均起飞延误:
SELECT departure_airport,
ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min,
COUNT(*) AS departed
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
ORDER BY avg_delay_min DESC
LIMIT 3;
| departure_airport | avg_delay_min | departed |
|---|---|---|
| ULY | 24.4 | 97 |
| BQS | 22.8 | 31 |
| LPK | 22.7 | 22 |
这几个「最差」的机场只有 22 到 97 个航班,一两次长时间延误就能左右平均值。在 HAVING 里设一个最小组规模,比较会公平得多:
SELECT departure_airport,
COUNT(*) AS departed,
ROUND(AVG(EXTRACT(EPOCH FROM actual_departure - scheduled_departure) / 60), 1) AS avg_delay_min
FROM flights
WHERE status IN ('Arrived', 'Departed')
GROUP BY departure_airport
HAVING COUNT(*) >= 300
ORDER BY avg_delay_min DESC
LIMIT 5;
| departure_airport | departed | avg_delay_min |
|---|---|---|
| SVX | 348 | 14.7 |
| VKO | 870 | 13.4 |
| BZK | 306 | 13.2 |
| KJA | 358 | 13.1 |
| LED | 960 | 12.6 |
注意分工:WHERE 决定哪些航班算作已起飞,HAVING 决定哪些机场的航班数足够多。
4. 本该用 COUNT(column) 却用了 COUNT(*)
COUNT(*) 数行数,COUNT(column) 数非 NULL 值。在分组查询里,这个差别往往就是重点:
SELECT status, COUNT(*) AS flights, COUNT(actual_departure) AS with_departure
FROM flights
GROUP BY status
ORDER BY flights DESC;
| status | flights | with_departure |
|---|---|---|
| Arrived | 16707 | 16707 |
| Scheduled | 15383 | 0 |
| On Time | 518 | 0 |
| Cancelled | 414 | 8 |
| Departed | 58 | 58 |
| Delayed | 41 | 0 |
有八个已取消的航班却记录了起飞时间。这是数据里的一个怪现象,只用 COUNT(*) 的话你永远不会发现。在这个查询上加 HAVING COUNT(actual_departure) > 0,会保留三组,而不是六组。
5. 分组表达式和选出的表达式不一致
如果按日期分组,分组用的表达式要和 SELECT 里的一样:
SELECT CAST(scheduled_departure AS date) AS day, COUNT(*) AS cancelled
FROM flights
WHERE status = 'Cancelled'
GROUP BY CAST(scheduled_departure AS date)
HAVING COUNT(*) >= 15
ORDER BY day;
| day | cancelled |
|---|---|
| 2026-09-07 | 18 |
| 2026-09-08 | 21 |
| 2026-09-09 | 51 |
| 2026-09-10 | 287 |
如果直接按原始的 scheduled_departure 时间戳分组,就会每个起飞时刻一组,HAVING 什么也找不到。(时刻表最末尾的取消堆积值得在报告「危机」之前多看一眼:414 次取消中有 403 次都在 9 月的未来计划里。)
没有 GROUP BY 的 HAVING
如果查询里有聚合函数和 HAVING,但没有 GROUP BY,整个结果被视为一组。查询要么返回一行,要么什么都不返回:
SELECT COUNT(*) AS flights
FROM flights
HAVING COUNT(*) > 30000;
| flights |
|---|
| 33121 |
把阈值改成 40,000,就返回零行。这在数据检查里偶尔有用:写一个只在出问题时才返回一行的查询。
快速判断清单
- 条件里用到了
COUNT、SUM、AVG、MIN、MAX或其他聚合函数吗?写进HAVING。 - 条件针对的是单行的值(状态、日期、某个航段的价格)吗?写进
WHERE。 - 条件只涉及分组列吗?两处都能用,优先用
WHERE。 SELECT里的每一列都出现在GROUP BY中或者在聚合函数里吗?如果不是,查询就是错的。- 在 PostgreSQL 里,你在
HAVING中重复写了聚合表达式,而不是用了它的别名吗?
如果涉及 JOIN,在相信任何 SUM 或 COUNT 之前先检查行数有没有翻倍,详见 SQL JOIN 用法详解。如果你想在每一行旁边放一个聚合值、而不是把行合并,你需要的是 窗口函数,而不是 GROUP BY。想直接动手练,可以做 SQL 练习题 里的第 4 到 8 题。
常见问题
没有 GROUP BY 能用 HAVING 吗?
能。整个结果变成一组,查询根据 HAVING 条件要么返回一行,要么什么都不返回。
HAVING 比 WHERE 慢吗?
针对聚合值的条件别无选择,谈不上快慢。针对原始列的条件,WHERE 能让数据库在分组之前就丢掉行,绝不会更慢,往往还更快。只要条件不需要聚合函数,就用 WHERE。
HAVING 里能用列别名吗?
在 PostgreSQL 和标准 SQL 里不能:HAVING 在 SELECT 之前执行,那时别名还不存在。要把表达式再写一遍。MySQL 等部分数据库作为扩展功能允许使用别名。
WHERE 和 HAVING 能在同一个查询里一起用吗?
能,而且经常一起用:WHERE 挑出参与聚合的行,HAVING 挑出要保留的组。上面关于起飞航班和延误的例子就两者都用了。
GROUP BY 和 DISTINCT 有什么区别?
SELECT DISTINCT status FROM flights 和 SELECT status FROM flights GROUP BY status 返回同样的六个值。GROUP BY 还能按组计算聚合值,并用 HAVING 筛选分组;DISTINCT 只负责去掉重复行。
其他语言: Español, Português (Brasil), Tiếng Việt, English