图 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,语句能跑,但过滤发生在了错误的时机,想看的明细维度早已在分组时丢失,而数字本身看上去还像模像样。
图 2:执行流水线上两个子句的位置与分工。
判断口诀与答题结构。
口诀只有一句:条件里出现聚合函数,只能放 HAVING;条件只看单行字段,一律放 WHERE。再补一个推论:同一个查询可以两者都用,WHERE 先行过滤、HAVING 再筛组。
回到面试现场,建议的答题结构:先给结论 HAVING,紧接着讲执行时机(WHERE 在分组前作用于行、聚合值尚不存在,HAVING 在聚合后作用于组),然后用「单笔超 1000 且合计超 2000」这种组合条件演示两者分工,最后补性能理由——HAVING 几乎最后执行且不做优化,行级条件放错位置等于逼着数据库对全量数据分组。你在业务 SQL 里抓到过聚合条件写错位置的 bug 吗?它是当场报错,还是悄悄给出了错误结果?欢迎在评论区分享。