有时候我们不只是要分组数据然后过滤结果,还得加点额外的逻辑——比如,拿学生在小组里的平均分和某个外部标准比一比。这时候 HAVING 里的子查询就派上用场了——这是个很强大的工具,可以让你直接在 SQL 查询里做更聪明的决策。
回顾一下 HAVING
我们来专注聊聊怎么把子查询和 HAVING 结合起来用,来在聚合值的层面过滤数据。为啥要这样?因为 WHERE 只能过滤单行,而 HAVING 是针对分组后的数据——这是另一种分析方式,让你能玩出更多花样。
在我们深入聊子查询和 HAVING 的组合之前,先复习下 HAVING 是啥,它和 WHERE 有啥区别。
WHERE在 分组前 过滤行(GROUP BY之前)。HAVING在 聚合后 过滤数据,也就是数据已经分组完了。
想象你在分析学生和他们的分数。用 WHERE 你可以把分数低于某个值的学生排除掉,而 HAVING 则能让你根据平均分或最高分把整个小组都排除。
数据示例
下面是一个学生表:
表 students:
| student_id | student_name | department | grade |
|---|---|---|---|
| 1 | Alex | Physics | 80 |
| 2 | Maria | Physics | 85 |
| 3 | Dan | Math | 90 |
| 4 | Lisa | Math | 60 |
| 5 | John | History | 70 |
HAVING 用法示例(不带子查询)
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > 75;
结果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
| Math | 75.0 |
“History” 这个院系没被选出来,因为它的平均分低于 75。很简单对吧?现在我们加点子查询的魔法。比如下面这个例子,我们可以按所有院系的平均分来过滤。
HAVING 里的子查询
HAVING 里的子查询是让你过滤聚合数据更灵活的好办法。你可以把平均分、最大值这些聚合结果和数据库其它地方算出来的值比一比。简单说,就是可以问:“我们这个结果是不是比全校平均还牛?”
例子:按平均分过滤院系
假设我们想找出那些学生学得比别人好的院系——也就是院系的平均分高于全校平均。
数据如下:
表 students:
| student_id | student_name | department | grade |
|---|---|---|---|
| 1 | Alex | Physics | 80 |
| 2 | Maria | Physics | 85 |
| 3 | Dan | Math | 90 |
| 4 | Lisa | Math | 60 |
| 5 | John | History | 70 |
先查一下所有学生的平均分:
SELECT AVG(grade) AS university_avg
FROM students;
现在在 HAVING 里用子查询:
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > (SELECT AVG(grade) FROM students);
结果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
这里发生了啥?
- 子查询(
SELECT AVG(grade) FROM students)算出全校平均分——这里是 77。 - 主查询按院系分组,算每个院系的平均分。
HAVING拿院系平均分和全校平均比,只保留更高的那些。
对比 WHERE 和 HAVING 的用法
为了更清楚区别,假设你想选出分数高于平均分的学生。只能用 WHERE:
SELECT name, grade
FROM students
WHERE grade > (SELECT AVG(grade) FROM students);
结果(用上面表的数据):
| name | grade |
|---|---|
| Alex | 80 |
| Maria | 85 |
| Dan | 90 |
但如果你想看哪些院系的平均分高于全校平均,就必须用 HAVING——因为你过滤的是分组,不是单行:
SELECT department, AVG(grade) AS avg_grade
FROM students
GROUP BY department
HAVING AVG(grade) > (SELECT AVG(grade) FROM students);
结果:
| department | avg_grade |
|---|---|
| Physics | 82.5 |
总结一下:
WHERE针对分组前的单行。HAVING针对聚合后的分组。
例子:多个聚合一起用
再来看一个场景。假设我们有 students 表,存着学生分数和院系:
表 students:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
现在我们想找出那些院系:
- 学生平均分 高于 全校平均。
- 院系里最高分 大于 90。
写个这样的查询:
SELECT department, AVG(grade) AS avg_grade, MAX(grade) AS max_grade
FROM students
GROUP BY department
HAVING AVG(grade) > ( SELECT AVG(grade) FROM students )
AND MAX(grade) > 90;
这个查询干了啥:
AVG(grade)> (SELECT AVG(grade) FROM students) —— 检查院系平均分是不是比全校高。MAX(grade)> 90 —— 说明有学生考得特别牛。
结果:
| department | avg_grade | max_grade |
|---|---|---|
| Math | 92.5 | 95 |
只有 “Math” 院系同时满足平均分高于全校、还有牛人分数大于 90。
例子:选出分差最小的组
假如你想找出那些组,组内最高分和最低分的差,比全校的分差还小。
我们用的还是 students 表:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
分两步来:
- 先算全校的最大-最小分差:
SELECT MAX(grade) - MIN(grade) AS range_university FROM students; - 然后写主查询,把它和子查询结合:
SELECT department, MAX(grade) - MIN(grade) AS range_department
FROM students
GROUP BY department
HAVING (MAX(grade) - MIN(grade)) < ( SELECT MAX(grade) - MIN(grade) FROM students );
查询结果:
| department | range_department |
|---|---|
| Physics | 5 |
| Math | 5 |
“Physics” 和 “Math” 这两个组的分数更稳定——分差比全校还小。
用 HAVING 和子查询优化查询
要记住,嵌套子查询在大数据库里可能会拖慢性能。给你几个建议:
用索引。 如果子查询用到的列在 WHERE 或 JOIN 里,记得给这些列加索引。
避免数据量爆炸。 如果子查询返回太多中间结果,拆成几步或者用临时表。
用 EXPLAIN 分析查询。 总是看看 PostgreSQL 怎么执行你的查询。如果发现子查询被执行了很多次,考虑怎么优化。
和 CTE 比一比。 有时候用 WITH(Common Table Expressions)会更快,也更好读。这个以后讲 :P
组合子查询、HAVING 和 GROUP BY
在 HAVING 里用子查询可以写出更复杂的过滤,特别是你要同时考虑聚合、平均值和别的指标时。这些都能帮你从真实数据里挖出有意思的洞见。
例子:按平均分和学生数对比院系
假设你想选出那些院系:
- 平均分高于全校平均。
- 学生人数比平均分最低的院系还多。
原始表 students:
| name | grade | department |
|---|---|---|
| Alex | 80 | Physics |
| Maria | 85 | Physics |
| Dan | 90 | Math |
| Olga | 95 | Math |
| Ivan | 70 | History |
| Nina | 75 | History |
| Oleg | 60 | History |
查询:
SELECT department, AVG(grade) AS avg_grade, COUNT(*) AS student_count
FROM students
GROUP BY department
HAVING AVG(grade) > ( SELECT AVG(grade) FROM students )
AND COUNT(*) > (
SELECT COUNT(*)
FROM students
GROUP BY department
ORDER BY AVG(grade)
LIMIT 1
);
这个查询展示了怎么在 HAVING 和 GROUP BY 里组合子查询来多条件分析。结果:
| department | avg_grade | student_count |
|---|---|---|
| Physics | 82.5 | 2 |
| Math | 92.5 | 2 |
History 院系没被选出来,因为它平均分最低,学生也最少。Physics 和 Math 都是分数和人数都高于平均。
常见错误和怎么避免
NULL 的坑。 如果数据里有 NULL,HAVING 里的子查询可能会出奇怪结果。用 COALESCE 处理下:
SELECT AVG(grade)
FROM students
WHERE grade IS NOT NULL;
子查询返回太多数据。 子查询结果太多会拖慢性能。一定要加好条件。
搞错执行顺序。 记住 HAVING 是在分组后执行的,子查询可能在主查询前就跑了。
没加索引。 子查询用到的列没索引会让查询很慢。
HAVING 里的子查询让你能在聚合层面做各种分析。你可以按复杂条件过滤分组、对比组间结果,写出很牛的分析查询。恭喜你,现在可以把这些知识用到实际项目里啦!
GO TO FULL VERSION