WHERE 与 HAVING 的分工
两者都在过滤,但作用对象不同:WHERE 过滤行,发生在分组与聚合之前;HAVING 过滤组,发生在分组与聚合之后,所以条件里可以写聚合函数。判断一个条件该写哪一层,看它会不会改变参与聚合的数据量:会改变就放 WHERE,不会改变就放 HAVING。同时出现时,先 WHERE 缩小数据集,再 GROUP BY,最后 HAVING 筛组,这个顺序在效率与可读性上都最好。
基本信息
| 过滤对象 | WHERE 过滤行,HAVING 过滤组 |
|---|---|
| 执行顺序 | WHERE 早于 GROUP BY,HAVING 晚于 GROUP BY |
| 聚合函数 | WHERE 不允许写,HAVING 允许写 |
| 列别名 | WHERE 不可用,HAVING 与 ORDER BY 可用 |
| 无 GROUP BY 时 | HAVING 仍生效,整表视为一个分组 |
| 适用平台 | 标准 SQL,MySQL 8.0 与 PostgreSQL 一致 |
典型使用场景
- 报表分层过滤:先按时间与状态过滤行,再按金额门槛过滤组
- 统计有效数据:WHERE 先剔除无效记录,聚合结果才可信
- 筛高频用户:HAVING COUNT(*) >= 5 属于组级条件
- 排查统计偏差:口径不对时先确认条件写在了哪一层
- 优化慢报表:把能提前过滤的条件从 HAVING 挪到 WHERE
- 数据质量检查:HAVING COUNT(*) > 1 找出重复分组
对比示例
对比示例
-- 两种写法结果相同,第一种效率更高
SELECT city FROM users WHERE status = 1 GROUP BY city;
SELECT city FROM users GROUP BY city HAVING MAX(status) = 1; # 不推荐
-- 两层过滤各司其职
SELECT city, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders
WHERE created_at >= '2024-05-01' # 行级过滤,减少参与聚合的数据
GROUP BY city
HAVING SUM(amount) > 10000 # 组级过滤
ORDER BY total DESC;逐项对照
| 维度 | WHERE | HAVING |
|---|---|---|
| 过滤对象 | 行 | 分组结果 |
| 执行时机 | 分组与聚合之前 | 分组与聚合之后 |
| 能否用聚合函数 | 不能 | 可以 |
| 能否用 SELECT 别名 | 不能 | 可以 |
| 对代价的影响 | 直接减少扫描与聚合量 | 不减少聚合量 |
| 典型写法 | WHERE status = 1 | HAVING COUNT(*) > 5 |
把行级条件写进 HAVING 会让全表参与聚合,大表上表现为临时表膨胀与磁盘排序,这是慢报表最常见的结构性问题之一。
ONLY_FULL_GROUP_BY 与 PostgreSQL 都要求 HAVING 里的非聚合条件依赖分组列,写条件时按分组键来组织更稳。
为了省事把条件全塞进 HAVING,会让中间结果集远大于必要规模;先想清楚条件属于行还是组,再决定放哪一层。
实践要点
- 先写 WHERE 再写 HAVING:过滤顺序决定扫描量
- HAVING 只放必须依赖聚合结果的条件
- 报表明细里把两层条件的用途写成注释,方便后续维护
- 慢报表优先检查是不是把行级条件写进了 HAVING
- 需要多处复用同一口径时,把它固化到视图或 CTE 里
- 用 EXPLAIN 确认 WHERE 上的条件确实走了索引