SQL执行顺序为:FROM → WHERE → GROUP BY → 聚合函数 → HAVING;WHERE在分组前过滤行,可走索引、大幅减少后续计算量,HAVING在分组聚合后过滤组,不走索引、必须全量计算。

因为 HAVING 必须等 GROUP BY 和所有聚合计算完成之后才执行,而 WHERE 在分组前就筛掉了大量原始行——这不是“稍慢一点”,是阶段性的计算量差异。
WHERE 在分组前过滤,HAVING 在分组后过滤
数据库执行顺序固定为:FROM → WHERE → GROUP BY → 聚合函数(如 COUNT(*)、SUM(amount))→ HAVING。这意味着:
-
WHERE看到的是原始表的每一行,能直接丢弃不匹配的记录,后续分组和聚合的数据量大幅减少 -
HAVING面对的是已分好组、且每组都算完COUNT(*)或AVG()的中间结果集,哪怕你只想保留 5% 的分组,数据库也得先把 100% 的分组全算出来 - 常见反模式:
SELECT user_id, COUNT(*) FROM logs GROUP BY user_id HAVING created_at > '2024-01-01'——created_at是单行字段,根本不能出现在HAVING中,MySQL 严格模式下直接报错,PostgreSQL 拒绝执行
WHERE 可走索引,HAVING 完全不走索引
HAVING 条件基于聚合结果(如 SUM(amount))或分组键,这些值不存在于原表中,数据库无法利用任何索引加速筛选:
-
WHERE status = 'paid':如果status有索引,可能只扫描几千行 -
HAVING status = 'paid':语法错误;即使强行写成HAVING COUNT(*) > 10,数据库也得先生成上万组,再逐个比对,全程无索引参与 - 真正起作用的是
WHERE中字段的索引 +GROUP BY字段的索引组合——比如WHERE order_date > ? GROUP BY user_id,理想索引是(order_date, user_id)
别名和非分组字段在 HAVING 中不可靠
HAVING 解析发生在 SELECT 列别名生成之前,因此:
-
SELECT user_id, COUNT(*) AS cnt GROUP BY user_id HAVING cnt > 10在 PostgreSQL 和 MySQL 严格模式下均报错 - 必须写成
HAVING COUNT(*) > 10;若表达式复杂(如ROUND(AVG(score), 2)),HAVING中也得完全重复,不能省 - 引用未出现在
GROUP BY中的原始列(如HAVING department = 'Sales')是逻辑错误:你没按department分组,怎么定义“这个组的 department”?MySQL 宽松模式可能允许,但换环境必崩
最易被忽略的一点:当 GROUP BY 字段基数极高(比如按 order_id 分组),HAVING COUNT(*) > 1 这类条件永远不成立,但数据库仍会为每行生成一个单行分组、再计算一次 COUNT(*)——优化器不会跳过这种明显无效的聚合,它照算不误。










