跳转到主要内容

35 HAVING

使用 HAVING 筛选聚合分组,并区分分组谓词与行筛选。

WHERE 在分组之前筛选单独的行;HAVING 在聚合计算完成后筛选分组。如果你想表达“订单数超过三笔的客户”,那是 HAVING——只有得到计数后,才能测试 count(*) > 3。如果你想表达“在 2024 年下过订单的客户”,那是 WHERE——年份是每一行的属性。

本章要点

  • 使用 HAVING 筛选分组
  • 区分 WHERE(逐行)与 HAVING(逐组)
  • 在同一查询中组合 WHERE 和 HAVING
  • 在 HAVING 中使用聚合函数
  • 为什么把谓词下推到 WHERE 通常更快

示例

示例 35.1 HAVING
-- HAVING with one aggregate condition
SELECT customer_id, count(*) AS order_count, sum(total) AS total_spent
FROM orders_example
GROUP BY customer_id
HAVING sum(total) > 30
ORDER BY customer_id;

-- HAVING with multiple conditions
SELECT customer_id, count(*) AS order_count, sum(total) AS total_spent
FROM orders_example
GROUP BY customer_id
HAVING count(*) >= 1 AND sum(total) > 30;

-- WHERE then HAVING: filter rows first, group, then filter groups
SELECT customer_id, count(*) AS order_count
FROM orders_example
WHERE total > 20                      -- per-row filter
GROUP BY customer_id
HAVING count(*) >= 1;                 -- per-group filter

-- HAVING without aggregates is legal but pointless — same as WHERE
SELECT customer_id
FROM orders_example
GROUP BY customer_id
HAVING customer_id > 50;              -- could just be WHERE

执行顺序有助于记住区别:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。把行谓词写在 WHERE 中,意图更清楚,也能减少进入分组的数据;PostgreSQL 也可以把合适的 HAVING 谓词下推。尽可能把更多筛选放进 WHERE。

HAVING 允许直接写聚合谓词:HAVING avg(amount) > 100、HAVING count(DISTINCT product_id) > 5。这些条件不能出现在 WHERE 中,因为聚合在那里还不存在。

最后一个查询在技术上有效,但有隐患:在 HAVING 中筛选非聚合列,效果与 WHERE 相同,却在流水线更晚的阶段进行。为了清晰起见,优先使用 WHERE;规划器通常也能把这个分组列谓词下推。

运行示例

运行方式(需要连接课程中的 orders_example 表):

$ psql -f source/having.sql postgres
 customer_id | order_count | total_spent
-------------+-------------+-------------
           1 |           2 |       80.00
(1 row)
 ...

常见陷阱

  • 写 WHERE count(*) > 3——WHERE 不允许使用聚合函数。请使用 HAVING。
  • 写 HAVING amount > 100 只有在 amount 已被分组或函数依赖于分组时才有效。要筛选单独的输入行,请使用 WHERE amount > 100。规划器通常会重写这一点,但读者会感到困惑。
  • 忘记 HAVING 在 GROUP BY 之后运行,因此引用的任何列都必须是 GROUP BY 列,或位于聚合函数内。
  • 在 count(*) FILTER (WHERE ...) 这种逻辑中使用 HAVING 而不是 WHERE。FILTER 针对每个聚合,位于 SELECT 中;HAVING 针对每个分组,决定保留哪些组。
提示

如果你想要“按某个聚合值排名的前 N 个分组”,可以组合 GROUP BY + ORDER BY agg DESC + LIMIT N——不需要 HAVING。HAVING 用于谓词(“至少 3 个”),而不是排名(“前 3 个”)。

动手练习

修改阈值:HAVING sum(total) < 50,看看结果集如何变化。然后把筛选移到 WHERE:WHERE total > 20 GROUP BY customer_id;,观察计数下降,因为进入分组的行变少了。

下载

SQL 文件:having.sql · 全部示例文件 · 准备工作