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,但数据库按下面的逻辑顺序处理各个子句:

  1. FROM 和 JOIN:构造行集合
  2. WHERE:丢弃行
  3. GROUP BY:形成分组
  4. 按组计算聚合函数
  5. HAVING:丢弃组
  6. SELECT:计算输出列和别名
  7. DISTINCT
  8. ORDER BY
  9. LIMIT / OFFSET

几乎所有 GROUP BY / HAVING 的错误都能从这张清单推出来:

常见错误

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,就返回零行。这在数据检查里偶尔有用:写一个只在出问题时才返回一行的查询。

快速判断清单

如果涉及 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