恭喜你,终于来到真正有趣的部分啦!今天我们要看看怎么把不同类型的子查询组合起来,解决更难的任务。EXISTS、IN、HAVING —— 这三兄弟能让你在数据库里玩出魔法。我们会从一张表里查数据,再用另一张表的数据来筛选、分组,然后再过滤分组。最后还会聊聊怎么让查询更高效的小技巧。
咱们先来定个总目标,这节课会一步步把它搞定。
任务描述
假设我们有一个大学数据库,里面有三张表:
表 students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
表 courses
| id | name |
|---|---|
| 1 | 数学 |
| 2 | 编程 |
| 3 | 哲学 |
表 enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
我们要选出所有满足以下条件的学生:
- 至少选了一门课
EXISTS。 - 至少有一门课没有成绩
IN。 - 属于平均分大于 80 的小组
HAVING。
用 EXISTS 和 IN 的解法
第1步:检查学生是否选课(EXISTS)。 先来最简单的条件。我们要知道哪些学生至少选了一门课。用 EXISTS 就行。
SELECT name
FROM students s
WHERE EXISTS (
SELECT 1
FROM enrollments e
WHERE e.student_id = s.id
);
- 外层查询从
students表里选出名字。 - 子查询检查
enrollments表里有没有和外层学生对应的记录(WHERE e.student_id = s.id)。 SELECT 1只是说明我们只关心有没有记录,不在乎内容。
结果:
| name |
|---|
| Otto |
| Maria |
| Alex |
现在我们知道谁选了课。但我们还想进一步筛选——只要那些有课没成绩的学生。
第2步:检查有没有成绩(IN + NULL)。 现在加个过滤:只要那些至少有一门课没成绩的学生。这里用 IN 和 NULL。
SELECT name
FROM students s
WHERE id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- 外层查询选出学生名字。
- 子查询从
enrollments表里选出grade IS NULL的student_id。
结果:
| name |
|---|
| Otto |
所以 Otto 是唯一有课没成绩的学生。是不是有点戏剧性!不过还没完,我们还得只看那些平均分大于 80 的小组。
用 HAVING 的解法
第3步:分组和用 HAVING 过滤。
现在要把所有条件合起来。我们要:
- 算出每个小组的平均分。
- 只要平均分大于 80 的小组。
- 输出这些小组里、同时满足前面条件的学生。
SELECT name
FROM students s
WHERE s.group_id IN (
SELECT group_id
FROM students
JOIN enrollments ON students.id = enrollments.student_id
WHERE grade IS NOT NULL
GROUP BY group_id
HAVING AVG(grade) > 80
)
AND id IN (
SELECT e.student_id
FROM enrollments e
WHERE e.grade IS NULL
);
- 外层查询选出所有满足条件的学生名字。
WHERE里的第一个子查询返回平均分大于 80 的group_id。- 我们把
students和enrollments联表,拿到成绩。 - 只要
grade IS NOT NULL的记录。 - 按
group_id分组。 - 用
HAVING过滤分组。
- 我们把
- 第二个子查询检查学生有没有课没成绩。
- 两个条件用
AND组合。
结果:
| name |
|---|
| Otto |
所以 Otto 不仅是唯一没成绩的学生,还属于一个平均分很高的小组,厉害吧!
方法对比:EXISTS vs IN
EXISTS 最适合用来快速判断有没有记录。它很高效,因为一旦找到一条就停了。大表里尤其有用。
而 IN 更适合你关注数据内容的时候。比如你要拿一堆 id 去做后续筛选。但要注意,如果子查询返回太多值,IN 可能会变慢。
什么时候用 HAVING
聚合数据、需要按结果过滤时,HAVING 是首选。但如果能把条件提前到 WHERE(比如按列过滤),那样会更简单也更快。
完整例子
再来一个例子巩固下:选出那些有学生成绩低于 75,但没人选“哲学”课的小组。
再提醒下我们的表:
表 students
| id | name | group_id |
|---|---|---|
| 1 | Otto | 101 |
| 2 | Maria | 101 |
| 3 | Alex | 102 |
| 4 | Anna | 103 |
表 courses
| id | name |
|---|---|
| 1 | 数学 |
| 2 | 编程 |
| 3 | 哲学 |
表 enrollments
| student_id | course_id | grade |
|---|---|---|
| 1 | 1 | 90 |
| 1 | 2 | NULL |
| 2 | 1 | 85 |
| 3 | 3 | 70 |
SELECT DISTINCT group_id
FROM students s
WHERE group_id IN (
SELECT s.group_id
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.grade < 75
)
AND group_id NOT IN (
SELECT s.group_id -- 一级嵌套查询
FROM students s
JOIN enrollments e ON s.id = e.student_id
WHERE e.course_id = (
SELECT id FROM courses WHERE name = '哲学' -- 二级嵌套查询 :P
)
);
- 第一个子查询选出有学生成绩低于 75 的小组。
- 第二个子查询排除和“哲学”课有关的小组。
- 我们用
IN和NOT IN组合条件,得到最终结果。
结果:
| group_id |
|---|
| 101 |
这些技巧有啥用?
实际工作中,这些方法能帮你分析复杂的数据关系。比如:
- 做数据分析时,找出“特殊”客户群(VIP、问题用户啥的)。
- 做推荐系统开发时,按一堆条件筛用户。
- 面试时,让你优化复杂 SQL 查询。
多练练吧!这就是你成为高手的路。
GO TO FULL VERSION