深蓝黑背景封面:右侧大标题「聚合后再过滤,WHERE 还是 HAVING」,副标题「WHERE 过滤行,HAVING 过滤组」;左侧示意图为一张小型订单表(客户、金额两列,四行数据:甲 900、甲 800、乙 1500、乙 1300),经箭头指向按客户分组的两个组卡片:组甲合计 1700 用青色描边并打勾表示被 HAVING 留下,组乙合计 2800 用灰色描边并打叉表示被整组过滤,下方标注过滤条件「HAVING SUM(金额) < 2000」;左下角注明「WHERE 在分组前逐行过滤,根本看不到 SUM」;左上标注「笔试复盘 · 数据库」,右上圆角胶囊标注「Day 04 · 11-11」 图 1:本文封面,按客户分组后,组甲被留下、组乙被整组过滤。

先说明属性:这是一篇笔试真题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题面:有一张订单表(客户、金额),要筛选「订单总额少于 2000 的客户」,过滤条件 SUM(金额) < 2000 应该写在 WHERE 子句还是 HAVING 子句?答案是 HAVING。但这类选择题真正考的不是答案,而是两个子句的执行时机。

一句话版本:WHERE 过滤的是行,HAVING 过滤的是组。

推导从 SQL 的逻辑执行顺序开始。一条带分组的查询,逻辑上先按 FROM 找到表,再按 WHERE 逐行过滤,然后才按 GROUP BY 分组、对每组计算聚合,最后由 HAVING 对组做筛选。关键在时机:WHERE 执行的那一刻,分组还没有发生,每一行都只携带自己的金额,「订单总额」这个值根本不存在。所以把 SUM(金额) < 2000 写进 WHERE,不是结果错,而是语句本身就非法。MySQL 官方文档把话说得很死:WHERE 表达式里可以使用 MySQL 支持的任何函数和运算符,聚合(分组)函数除外。

HAVING 则站在流水线的另一端。官方文档描述它在「结果发往客户端之前」的阶段应用,此时组已经形成、聚合值已经算好,它的过滤对象是组,自然可以引用聚合函数。顺序的代价文档也点了名:HAVING 几乎最后执行、不做优化,所以行级条件塞给它既慢又错。

顺带补一个常被追问的细节:这里说的是「逻辑」执行顺序。优化器实际执行时可能重排步骤、用索引提前过滤,但结果必须与这条逻辑流水线等价——把它当作理解语义的思维模型,而不是数据库的物理执行计划。另外,即使不写 GROUP BY,HAVING 也是合法的:标准语义下所有行隐式构成一个组,整条聚合结果要么全留、要么全没。

正确写法与实测。

SELECT customer, SUM(amount) AS total
FROM orders
GROUP BY customer
HAVING SUM(amount) < 2000;

用真实数据库跑一遍:造四行数据——甲 900、甲 800、乙 1500、乙 1300。上面这条 HAVING 语句输出只有一行「甲 1700」(组乙合计 2800 被整组过滤)。再把同样的条件挪进 WHERE:

SELECT customer, SUM(amount) AS total
FROM orders
WHERE SUM(amount) < 2000
GROUP BY customer;

数据库直接报错「misuse of aggregate: SUM()」——聚合函数出现在它还不该存在的位置,连执行的机会都没有。

反过来看:行级条件必须回 WHERE。

官方文档明确建议:本该属于 WHERE 的条件不要放进 HAVING。一是语义分工:「只看单笔超过 1000 的订单」是行级条件,必须在分组前过滤;二是性能:WHERE 先过滤,进入分组的数据量更小,聚合成本更低。两个子句是合作关系,不是二选一。比如「单笔金额超过 1000 的订单,合计超过 2000 的客户」:

SELECT customer, SUM(amount) AS total
FROM orders
WHERE amount > 1000
GROUP BY customer
HAVING SUM(amount) > 2000;

实测输出「乙 2800」:甲的两笔订单都没过 1000,WHERE 阶段就被滤掉了,根本没进入分组。这个结果同时演示了两件事——WHERE 决定哪些行参与聚合,HAVING 决定哪些组留下。

值得强调的是,这类错误在真实业务里并不温和:「按维度聚合后过滤」是用户行为宽表、报表统计的日常操作。条件写错位置,轻则像上面实测那样当场报错;重则语义悄悄偏离预期——比如把行级条件写进 HAVING,语句能跑,但过滤发生在了错误的时机,想看的明细维度早已在分组时丢失,而数字本身看上去还像模像样。

深蓝黑背景概念图,标题「WHERE 在行上,HAVING 在组上」。左上方青色卡片给出正确写法 SQL:SELECT 客户、SUM(金额) AS 总额 FROM 订单表 GROUP BY 客户 HAVING SUM(金额) 小于 2000;右上方紫色卡片给出错误写法:把 SUM(金额) 小于 2000 写进 WHERE,并注明 WHERE 执行时还没有分组、SUM 尚不存在,数据库会直接报错。中部是五个环节的执行流水线:FROM 订单表、WHERE 行级过滤、GROUP BY 分组、HAVING 组级过滤、SELECT 输出,用箭头依次连接;WHERE 环节下方标注「过滤对象是行、时机在分组之前、不能用聚合函数」,HAVING 环节下方标注「过滤对象是组、时机在聚合之后、可以用聚合函数」。左下方数据卡片代入示例:甲 900 加 800 等于 1700 被留下,乙 1500 加 1300 等于 2800 整组过滤,结果集只剩甲一行。右下方记忆卡片:判断标准是条件里出现聚合函数就只能放 HAVING 图 2:执行流水线上两个子句的位置与分工。

判断口诀与答题结构。

口诀只有一句:条件里出现聚合函数,只能放 HAVING;条件只看单行字段,一律放 WHERE。再补一个推论:同一个查询可以两者都用,WHERE 先行过滤、HAVING 再筛组。

回到面试现场,建议的答题结构:先给结论 HAVING,紧接着讲执行时机(WHERE 在分组前作用于行、聚合值尚不存在,HAVING 在聚合后作用于组),然后用「单笔超 1000 且合计超 2000」这种组合条件演示两者分工,最后补性能理由——HAVING 几乎最后执行且不做优化,行级条件放错位置等于逼着数据库对全量数据分组。你在业务 SQL 里抓到过聚合条件写错位置的 bug 吗?它是当场报错,还是悄悄给出了错误结果?欢迎在评论区分享。