GROUP BY 与 HAVING
本教程共 50 篇 · 第 28 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
28. GROUP BY 与 HAVING
本节目标:学会用 GROUP BY 把数据分组成小堆分别聚合,用 HAVING 筛掉不符合条件的组,并分清 HAVING 和 WHERE 各自管什么。
上一章的聚合函数一次只吐一个汇总值。现实里我们常想”按用户分堆、每堆各算一笔”,这就用到了 GROUP BY。
延续前面的 orders 表:
CREATE TABLE orders (
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INT,
amount NUMERIC(10,2),
status VARCHAR(20),
created_at DATE
);
INSERT INTO orders (user_id, amount, status, created_at) VALUES
(1, 99.00, 'paid', '2025-02-01'),
(2, 150.50, 'shipped', '2025-04-10'),
(3, 20.00, 'pending', '2025-06-11'),
(1, 300.00, 'paid', '2025-08-22'),
(4, 75.25, 'cancelled','2025-10-05'),
(2, 60.00, 'shipped', '2025-11-02');
GROUP BY 分组统计
GROUP BY 列 会按该列的值把行分成若干”堆”,每堆值相同。然后你可以在 SELECT 里对每堆做聚合。
-- 每个用户下了多少笔订单、总金额多少
SELECT user_id,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
ORDER BY user_id;
解释一下:用户 1 有两笔订单(99 和 300),所以 order_count 是 2,total_amount 是 399。其他用户同理。
Note
GROUP BY里出现的列,才能单独写在SELECT里(其它列必须包在聚合函数里),否则 PostgreSQL 不知道该取堆里哪一行的值。
按多列分组
GROUP BY 也可以跟多列,组合相同才算同一组:
-- 按 (用户, 状态) 分组,看每组的笔数
SELECT user_id, status, COUNT(*) AS cnt
FROM orders
GROUP BY user_id, status
ORDER BY user_id, status;
按表达式分组
分组依据还能是表达式,比如按”注册月份”统计。下面用 DATE_TRUNC 把日期截断到月份:
-- 按订单月份分组,看每月总金额
SELECT DATE_TRUNC('month', created_at) AS month,
SUM(amount) AS month_total
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
GROUP BY 后面的表达式,和 SELECT 里写的那段要一模一样(或者至少是等价的写法),PostgreSQL 才能对上。
GROUP BY 的硬性规则
SELECT 里出现的每一列,要么写在 GROUP BY 里,要么包在聚合函数里。下面这种写法会直接报错:
-- 错误:username 既不在 GROUP BY,也没被聚合
SELECT user_id, username, COUNT(*)
FROM orders
GROUP BY user_id;
原因很简单:同一个 user_id 可能对应多行,PostgreSQL 不知道 username 该取哪一行。要么把 username 加进 GROUP BY(前提是它和 user_id 一一对应),要么用聚合比如 MIN(username)。
Warning这个规则很严格。写多了聚合查询后,报 “column must appear in the GROUP BY clause” 是最常见的错误之一。养成习惯:SELECT 里每多一列,先问自己”它在 GROUP BY 里,还是被聚合了?“
HAVING 过滤分组
WHERE 只能在分组之前过滤行,过滤不了”分组之后”的结果。要筛分组,就得用 HAVING,它作用在聚合结果上。
-- 只保留总金额超过 200 的用户组
SELECT user_id,
SUM(amount) AS total_amount
FROM orders
GROUP BY user_id
HAVING SUM(amount) > 200
ORDER BY total_amount DESC;
这里用户 1(399)和 2(210.50)达标,用户 3、4 被过滤掉。
Tip
HAVING里通常写聚合条件(比如SUM(...)>200、COUNT(*)>1)。也可以写普通列条件,但那种情况一般放在WHERE更高效,因为能更早减少参与分组的行数。
HAVING 与 WHERE 的区别
这是本章最关键的一点,记住这句话就够了:
WHERE过滤行,在分组之前生效。HAVING过滤组,在分组之后生效。
-- 先 WHERE 排除已取消订单,再按用户分组,
-- 最后 HAVING 只留总金额 > 100 的组
SELECT user_id,
SUM(amount) AS total_amount
FROM orders
WHERE status <> 'cancelled'
GROUP BY user_id
HAVING SUM(amount) > 100
ORDER BY total_amount DESC;
执行顺序是这样的:FROM → WHERE(挑行)→ GROUP BY(分组)→ HAVING(挑组)→ SELECT → ORDER BY → LIMIT。
Warning因为
HAVING在SELECT之前执行,所以不能在 HAVING 里用 SELECT 起的列别名。上面例子里只能重复写SUM(amount),而不能写HAVING total_amount > 100。
一次聚合多个维度:ROLLUP 与 GROUPING SETS
除了普通 GROUP BY,PostgreSQL 还支持一次算出多个层级的汇总。
ROLLUP:在分组基础上,额外加一行”总计”。GROUPING SETS:手动指定要算哪些分组组合。
-- ROLLUP:既按 user_id 分组,又多一行全部用户的合计
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY ROLLUP(user_id)
ORDER BY user_id;
NoteROLLUP 多出来的那行
user_id是NULL,代表”所有用户的汇总”。想算更复杂的多维交叉,可以用CUBE或GROUPING SETS,进阶章节会展开。
常见错误
- 在 SELECT 里写了没聚合也没分组的列,被 PostgreSQL 拒绝(报 GROUP BY 错误)。
- 把本该放 WHERE 的行级过滤写进 HAVING,白白多算了分组,拖慢查询。
- 忘记 GROUP BY 之后结果行数等于”组数”而不是原表行数,别拿它当明细行用。
- 在 HAVING 里引用 SELECT 起的别名,因为执行顺序问题会失败。
HAVING 里也常用 COUNT 做条件
除了 SUM,COUNT 也常出现在 HAVING 里。比如”只保留下过 2 笔以上订单的用户”:
SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 1
ORDER BY cnt DESC;
分组统计配合多表连接
真实报表常要跨表。比如想知道”每个用户花了多少钱”,把 orders 按 user_id 聚合后 JOIN users 取名字:
SELECT u.username,
COUNT(o.id) AS 订单数,
SUM(o.amount) AS 总消费
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username
ORDER BY 总消费 DESC NULLS LAST;
这里用 LEFT JOIN,能保证”没下过单的用户”也出现在结果里(总消费为 NULL)。注意 GROUP BY 要把 SELECT 里没聚合的列都列上。
GROUP BY 和 DISTINCT 的一点关系
有时 GROUP BY 某一列后只 SELECT 那一列,效果和 DISTINCT 类似:
-- 下面两句结果一样:都拿到去重后的 user_id
SELECT DISTINCT user_id FROM orders;
SELECT user_id FROM orders GROUP BY user_id;
但 GROUP BY 的真正价值是”边分组边聚合”,单纯去重用 DISTINCT 更直观。
Note除了 ROLLUP,PostgreSQL 还支持 CUBE 和 GROUPING SETS,能一次算出更多维度的交叉汇总,比如”按用户""按状态""按用户×状态”同时出。它们属于进阶用法,后面进阶章节会展开讲。另外,GROUP BY 的分组依据不一定要来自原表列,也可以是对列运算后的表达式,只要 SELECT 和 GROUP BY 里写的一致即可。
小结
GROUP BY 列按列分堆,每堆各自聚合。- SELECT 里的列要么在 GROUP BY 中,要么被聚合,否则报错。
- 多列分组看”组合”,表达式也能作分组依据。
HAVING过滤分组,条件常含聚合函数;ROLLUP可顺带算总计。WHERE管行、HAVING管组;HAVING 不能用 SELECT 别名。