首页 / PostgreSQL 入门教程 / GROUP BY 与 HAVING

PostgreSQL 入门教程

GROUP BY 与 HAVING

本教程共 50 篇 · 第 28 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

PostgreSQLPostgreSQL 入门教程GROUP BYHAVING分组统计聚合查询

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(...)>200COUNT(*)>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;

执行顺序是这样的:FROMWHERE(挑行)→ GROUP BY(分组)→ HAVING(挑组)→ SELECTORDER BYLIMIT

Warning

因为 HAVINGSELECT 之前执行,所以不能在 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;
Note

ROLLUP 多出来的那行 user_idNULL,代表”所有用户的汇总”。想算更复杂的多维交叉,可以用 CUBEGROUPING 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 别名。