having 用于过滤分组后的结果,where 不能实现此功能;where 在分组前执行,作用于原始行和源表字段,而 having 在 group by 和聚合后执行,可使用聚合函数、group by 列或部分数据库支持的别名。

WHERE 不能过滤分组后的结果,HAVING 才行
很多人写 GROUP BY 时想筛掉某些分组(比如销售额低于 1000 的部门),却把条件写在 WHERE 里,结果发现根本没生效——因为 WHERE 在分组前执行,它只能过滤原始行,无法访问聚合函数结果。
真正能对分组后数据做判断的,只有 HAVING。它在 GROUP BY 和聚合计算之后运行,所以可以安全使用 COUNT()、SUM()、AVG() 等。
-
WHERE作用于单行,字段必须是源表中存在的列(如order_date、status) -
HAVING作用于分组,字段可以是聚合函数(如COUNT(*))、别名(部分数据库支持,如 MySQL 8.0+,但 PostgreSQL 不支持直接用别名)、或GROUP BY中出现的列 - 如果同时有
WHERE和HAVING,顺序一定是:先WHERE→ 再GROUP BY→ 再聚合 → 最后HAVING
想忽略分组内部分记录?得用 WHERE 配合条件聚合
“忽略分组内的部分记录”这个需求,本质不是筛分组,而是控制哪些行参与聚合计算。比如:只统计「已完成」订单的金额总和,但还要按用户分组——这时不能靠 HAVING,得让这些“非完成”订单压根不进聚合过程。
常见做法是在聚合函数里嵌套 CASE WHEN,或者用 WHERE 提前过滤。两者语义不同:
- 用
WHERE status = 'completed':整行被排除,该用户的其他状态订单也不见了,且该用户若无完成订单,整个分组就消失 - 用
SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END):所有行都参与分组,只是非完成订单贡献 0,用户分组一定存在 - 如果要用
COUNT()统计“完成订单数”,别写COUNT(CASE ...),而要写COUNT(CASE WHEN status = 'completed' THEN 1 END)——因为COUNT()会忽略NULL,而ELSE 0会让非完成项变成 0(被计入),必须用ELSE NULL或直接省略ELSE
HAVING 报错 “column must appear in GROUP BY” 怎么办
这是 PostgreSQL 和 SQL 标准严格模式下的典型报错,意思是:你在 HAVING 里引用了一个既没出现在 GROUP BY 子句、也不是聚合函数的列(比如写了 HAVING user_name = 'Alice' 但没 GROUP BY user_name)。
解决方式取决于你的真实意图:
- 真想按某个字段筛选分组 → 把它加进
GROUP BY,再在HAVING里用(如GROUP BY dept, user_name HAVING user_name = 'Alice') - 其实只想筛某几个固定值的分组 → 改用
WHERE过滤源数据(如WHERE user_name = 'Alice'),更高效,也避免无谓分组 - 用到了聚合结果(如
HAVING SUM(amount) > 5000)→ 没问题,无需加进GROUP BY,这是HAVING的合法用法
MySQL 默认宽松模式可能不报错,但这不是标准行为,换环境容易翻车。
WHERE 和 HAVING 性能差异不止是顺序问题
表面上看,WHERE 先执行所以更快;但实际影响更细:如果 WHERE 能利用索引大幅减少扫描行数,后续 GROUP BY 和聚合开销就会显著下降。而 HAVING 是在内存或临时表中对已分组结果过滤,无法触发索引,纯 CPU 运算。
- 优先把可下推的过滤条件(如时间范围、状态码、ID 区间)放在
WHERE - 避免在
HAVING里写复杂表达式(如HAVING UPPER(dept) = 'SALES'),这会让整个分组结果都得计算一遍 - 某些场景下,用子查询或 CTE 把
HAVING逻辑提前到WHERE层(比如先算出各分组聚合值,再 join 回原表),反而更可控
最常被忽略的一点:HAVING 之后没有“再分组”机会,一旦它筛掉了某个分组,那个分组的数据就彻底没了——不像 WHERE,你还能在它前面再加 JOIN 或子查询补回来。










