where不能用于过滤聚合结果,必须用having;where在聚合前过滤原始行,having在group by后过滤分组结果;优化应优先将条件下推至where,而非依赖having。

WHERE不能用在聚合结果上,这是语法错误不是性能问题
直接写 WHERE COUNT(*) > 10 会报错,因为 WHERE 在聚合执行前就过滤行,此时 COUNT(*) 还没算出来。数据库根本不会走到“性能对比”那步——它连语法检查都通不过。
常见错误现象:ERROR: column "count" does not exist 或类似提示,尤其在 PostgreSQL、MySQL 8.0+、SQL Server 中非常明显。
-
WHERE处理的是原始表的每一行,字段必须来自FROM子句中的表 -
HAVING处理的是GROUP BY后的分组结果,可以引用聚合函数和GROUP BY列 - 想筛“订单数大于5的用户”,必须用
HAVING COUNT(order_id) > 5,不能塞进WHERE
HAVING本身不慢,但滥用会导致全量聚合再过滤
HAVING 的代价取决于它前面有没有有效的 WHERE 预过滤。如果 WHERE 已经把数据从 1000 万行干到 2 万行,HAVING 只需对这 2 万行分组后的几百个组做判断;但如果 WHERE 是空的,就得先对全部 1000 万行做分组和聚合,再扔掉 99% 的组——这才是真正的性能瓶颈。
- 先用
WHERE status = 'paid'过滤出有效订单,再GROUP BY user_id HAVING COUNT(*) >= 3,比反过来快几个数量级 -
HAVING中的条件无法利用索引(除非是GROUP BY列本身,如HAVING user_id > 1000) - 某些旧版 MySQL(5.6 及更早)在
HAVING引用非聚合列时可能隐式触发临时表,加剧 IO
替代HAVING的几种实际优化手段
当 HAVING 成为慢查询瓶颈,优先考虑逻辑下推或结构重构,而不是硬扛。
- 把能前置的条件全塞进
WHERE:比如HAVING MAX(created_at) > '2024-01-01',通常可改写为WHERE created_at > '2024-01-01'再聚合 - 用窗口函数代替部分
HAVING场景:例如“找出每个部门工资前3的员工”,用ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC)比先GROUP BY再HAVING更直接 - 物化高频聚合结果:对“每日活跃用户数”这类固定维度,提前写入汇总表,查时直接
WHERE date = '2024-04-05'
MySQL与PostgreSQL在HAVING行为上的细微差异
MySQL 允许 HAVING 引用 SELECT 列别名(如 SELECT COUNT(*) AS cnt FROM t GROUP BY x HAVING cnt > 10),PostgreSQL 不允许,必须重复表达式或用子查询。这不是性能差异,而是语法宽容度不同,容易在迁移时踩坑。
- PostgreSQL 报错:
column "cnt" does not exist,必须写成HAVING COUNT(*) > 10 - MySQL 5.7 默认开启
sql_mode=ONLY_FULL_GROUP_BY后,也会限制HAVING引用非分组非聚合字段 - 两者都支持
HAVING中使用子查询,但性能极差,应避免(如HAVING COUNT(*) > (SELECT AVG(c) FROM (SELECT COUNT(*) c FROM t GROUP BY x) s))
HAVING 本身无效——它加速的是前面的 WHERE 和 GROUP BY 排序/哈希过程。真正该盯住的,是聚合前的数据量和分组键的选择性。










