where 应先过滤,因其在分组前执行,可大幅减少输入数据量;having 在分组和聚合后执行,仅用于筛选分组结果,避免将本该由 where 处理的条件(如 status != 'deleted')移至 having。

WHERE 和 HAVING 到底谁该先过滤?
别等 GROUP BY 完了再筛——能用 WHERE 干的事,绝不用 HAVING。因为 WHERE 在分组前就砍掉大量行,HAVING 是在所有分组完成、聚合计算做完之后才执行,多算一次 COUNT/SUM 就多一次开销。
常见错误:把本该在分组前排除的无效数据(比如 status = 'deleted')放到 HAVING 里判断,导致 MySQL 白分组、白聚合。
- 正确做法:先用
WHERE status != 'deleted' AND created_at >= '2025-01-01'缩小输入集 - 再
GROUP BY user_id,最后用HAVING COUNT(*) > 5筛活跃用户 - 如果表有千万级数据,
WHERE提前过滤掉 90%,GROUO BY性能可能提升 3–5 倍
HAVING 中写聚合表达式,别用别名
HAVING 子句里不能直接引用 SELECT 中定义的别名(比如 total_sales),MySQL(尤其 8.0 之前)会报 Unknown column 'total_sales' in 'having clause' 错误。
原因:SQL 执行顺序是 GROUP BY → 聚合计算 → HAVING → SELECT(含别名生成),HAVING 阶段别名还没诞生。
- 错:
HAVING total_sales > 10000 - 对:
HAVING SUM(quantity * price) > 10000 - 若表达式复杂,可考虑子查询或 CTE,但别为图省事硬套别名
多条件组合时,AND/OR 优先级和 NULL 处理要小心
HAVING 支持 AND/OR,但聚合结果为 NULL 时逻辑容易翻车。例如 HAVING COUNT(order_id) > 0 AND SUM(amount) > 1000,如果某组没订单,COUNT(order_id) 是 0,但 SUM(amount) 是 NULL,整个条件变成 TRUE AND UNKNOWN → 结果为 UNKNOWN,该组被过滤掉——这常被误认为“数据丢了”。
- 显式处理 NULL:
HAVING COUNT(order_id) > 0 AND COALESCE(SUM(amount), 0) > 1000 - 避免混合使用 AND/OR 不加括号,比如
HAVING SUM(a) > 10 OR AVG(b) 0优先级易混淆,建议加括号 - 测试时用
SELECT先查出原始分组结果,确认各聚合字段是否含 NULL
索引怎么配才能让 GROUP BY + HAVING 快起来?
MySQL 的 GROUP BY 默认会触发临时表 + 文件排序,HAVING 又没法走索引——但你可以让分组本身变快,间接减少 HAVING 的负担。
- 最有效:在
GROUP BY字段上建联合索引,顺序按分组字段 +WHERE过滤字段(如INDEX (status, user_id)) - 如果
HAVING里只依赖单个聚合(如COUNT(*)),且分组字段有索引,MySQL 8.0+ 可能用到松散索引扫描(Loose Index Scan),跳过部分行 - 避免在
GROUP BY中用函数,比如GROUP BY YEAR(created_at)会让索引失效;改用范围条件 + 拆分字段预计算
真正卡住性能的,往往不是 HAVING 本身,而是它前面那步 GROUP BY 没索引撑着。先盯紧执行计划里的 Using temporary; Using filesort,再动手优化。











