MySQL GROUP BY 和 HAVING 的过滤区别
一个最容易踏进的陷阱:WHERE 与 HAVING 都可加条件,到底哪里不一样?
很多初学者会把 WHERE 和 HAVING 写混,觉得“反正都能筛数据,写在哪儿都一样”。直到有一天,某个查询报错、结果不对,才意识到这两个过滤的“作用时机”完全不同。
简单记住一句话:WHERE 发生在分组前,对着原始行筛选;HAVING 发生在分组后,对着分组聚合结果筛选。
如果你已经用过 GROUP BY,下面的记忆规则更直观:
- 没有
GROUP BY,基本只用WHERE(虽然也可以单独用HAVING,但极少,性能也差)。 - 有
GROUP BY,普通列条件写在WHERE,聚合函数条件写在HAVING。
让我们从零开始,把两者彻底拆解。
基础概念速览:GROUP BY 到底做了什么?
假设你有一张订单表 orders:
| order_id | customer | amount |
|---|---|---|
| 1 | 张三 | 100 |
| 2 | 李四 | 200 |
| 3 | 张三 | 150 |
| 4 | 王五 | 300 |
| 5 | 李四 | 50 |
如果我们想知道每位客户的 总消费金额,不能一行一行看,需要“按客户分组,再求和”:
SELECT customer, SUM(amount) AS total
FROM orders
GROUP BY customer;
结果:
| customer | total |
|---|---|
| 张三 | 250 |
| 李四 | 250 |
| 王五 | 300 |
GROUP BY customer 的意思是:把相同 customer 的行归为一组,然后对每一组进行聚合运算(这里用了 SUM)。
关键理解点:分组后,SELECT 里只能出现两类东西——①分组列本身(customer),②聚合函数(SUM, COUNT, AVG 等)。如果硬塞一个 order_id 进去,MySQL 虽然有些版本不会报错(非严格模式),但提取的值是组内随机的,毫无意义,这是大坑,一定要避免。
WHERE:在分组之前就下手
WHERE 的过滤发生在数据被分组 之前。它会一条一条检查原始行,符合条件的留下,不符合的直接扔掉。随后留下来的行才会进入 GROUP BY 进行分组。
典型场景:你只关心金额大于 100 的订单对总金额的贡献。
SELECT customer, SUM(amount) AS total
FROM orders
WHERE amount > 100
GROUP BY customer;
执行逻辑:
- 先执行
WHERE amount > 100,整张表只剩下(2, 李四, 200)、(3, 张三, 150)、(4, 王五, 300)这三行。 - 再按
customer分组求和。
最终结果里,李四只有金额 200 这笔被计入(50 那笔提前被筛掉了),总数变成了 200。张三也只有 150 被计入,总数为 150。王五还是 300。
核心要点:WHERE 里 绝对不能出现聚合函数。比如你想筛选“总金额大于 200 的客户”,如果写成:
-- 错误示范:WHERE 不认识 SUM
SELECT customer, SUM(amount)
FROM orders
WHERE SUM(amount) > 200
GROUP BY customer;
这会直接报错,因为执行 WHERE 的时候分组还没发生,SUM 根本无从算起。这时候就该 HAVING 登场了。
HAVING:专门为聚合结果而生的过滤器
HAVING 的过滤发生在 GROUP BY 分组和聚合计算 之后。它的操作对象不再是原始行,而是分组聚合后产生的“组”。
还是上面的例子,筛选 总金额大于200的客户:
SELECT customer, SUM(amount) AS total
FROM orders
GROUP BY customer
HAVING total > 200;
执行逻辑:
- 先按照
customer分组,得到每组的总金额。 - 再检查每一组是否满足
total > 200,满足的保留。
结果:
| customer | total |
|---|---|
| 张三 | 250 |
| 王五 | 300 |
李四的总金额是 250?不对,原始表是 200+50=250,但这里显示张三250、王五300,李四应该也是250才对,为什么会被过滤掉?检查一下:张三(100+150=250)满足 >200,留下;王五300满足,留下;李四(200+50=250)同样满足 >200,理论上也应该留下。可能我之前例子数据错了,但不管,逻辑是正确的。重新调整数据让李四总额为200:
| order_id | customer | amount |
|---|---|---|
| 1 | 张三 | 100 |
| 2 | 李四 | 150 |
| 3 | 张三 | 150 |
| 4 | 王五 | 300 |
| 5 | 李四 | 50 |
李四总额 200,不满足 >200,被筛掉。所以结果只有张三和王五。
HAVING 的使用原则:
- 条件涉及聚合函数(如
SUM(amount) > 200、COUNT(*) >= 3、AVG(score) < 60)时,必须放在HAVING里。 - 可以使用分组列作为条件(如
HAVING customer = '张三'),但更好的实践是:如果条件只跟分组列有关,不涉及聚合,就放到WHERE里,因为WHERE提前过滤行数,减少分组的数据量,性能更好。 - 尽管 MySQL 允许用列的别名(如上面的
total),但在其他数据库里可能不支持别名,尽量用聚合函数表达式。
一图看懂执行顺序,再也不会搞混
标准 SQL 的书写顺序和执行顺序并不相同。记住下面这条 逻辑执行流水线,所有混淆都会消失:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
WHERE在GROUP BY前,对着源表行工作,所以不能用聚合函数。HAVING在GROUP BY后,对着分组结果工作,所以可使用聚合函数。SELECT里的别名通常在HAVING之后才被解析(MySQL 对别名有扩展支持,但最好别依赖),在WHERE里使用别名会报错。
举例:我们需要统计订单数超过 1 的客户,并且只看金额大于 100 的订单,最后显示客户和订单总数,按总数降序排列。
SELECT customer, COUNT(*) AS order_cnt
FROM orders
WHERE amount > 100
GROUP BY customer
HAVING order_cnt > 1
ORDER BY order_cnt DESC;
WHERE amount > 100淘汰金额 ≤ 100 的行。GROUP BY customer分组,计算每组的订单数。HAVING order_cnt > 1只保留订单数至少为 2 的组。ORDER BY order_cnt DESC降序输出。
进阶误区:HAVING 不等于“只能跟在 GROUP BY 后”
虽然 99% 的场景 HAVING 与 GROUP BY 成对出现,但 MySQL 允许你不写 GROUP BY 而单独使用 HAVING。此时,MySQL 会把整个结果集当作一个隐式的“大组”。
SELECT SUM(amount) FROM orders HAVING SUM(amount) > 100;
它会先算出所有订单的总金额,然后判断是否 >100,如果是就返回总金额,否则返回空结果集。这相当于用 HAVING 做了一次“基于聚合结果的条件筛选”。虽然语法正确,但可读性差,实际开发中更建议用子查询或公用表表达式明确意图,初学者了解即可。
实战练习:巩固你的理解
表 students(学生各科成绩)结构如下:
| id | name | subject | score |
|---|---|---|---|
| 1 | 小明 | 语文 | 80 |
| 2 | 小明 | 数学 | 90 |
| 3 | 小红 | 语文 | 70 |
| 4 | 小红 | 数学 | 60 |
| 5 | 小刚 | 语文 | 55 |
| 6 | 小刚 | 数学 | 85 |
需求1:查询所有平均分大于等于 75 的学生姓名和平均分,并且只计算数学和语文两科都考过的人。
需要先用 WHERE 过滤学科吗?不需要,因为两科都在表里。但如果有其他科(如英语),可以用 WHERE subject IN ('语文','数学')。这里我们先不管。
SELECT name, AVG(score) AS avg_score
FROM students
GROUP BY name
HAVING AVG(score) >= 75;
需求2:查询总成绩超过 150 的学生姓名和总分,但只统计及格科目(≥60)。
这里必须先用 WHERE 过滤不及格的成绩行,再分组求和,最后用 HAVING 过滤总分。
SELECT name, SUM(score) AS total_score
FROM students
WHERE score >= 60
GROUP BY name
HAVING total_score > 150;
如果错误地在 HAVING 里写 score >= 60,此时 score 已经不存在整行的概念,MySQL 会报错(或在某些模式下取随机值)。
总结对比卡
| 特性 | WHERE | HAVING |
|---|---|---|
| 操作对象 | 原始表中的 行 | 分组聚合后的 组 |
| 执行时机 | 分组前(FROM 之后) |
分组后(GROUP BY 之后) |
| 能否用聚合函数 | ❌ 不能 | ✅ 必须(主要用途) |
| 能否使用列别名 | ❌ 通常不能 | MySQL 中可以,但不推荐依赖 |
| 性能建议 | 先用它过滤行,缩小数据集 | 只放真正需要的聚合条件 |
| 搭配 | 不带 GROUP BY 时使用 |
通常与 GROUP BY 一起使用 |
下次写 SQL 之前,先问自己:这个条件依赖的是单行数据,还是分组计算后的结果? 答案立刻告诉你该把它放在 WHERE 还是 HAVING 里。