咱们再聊聊 SELECT 里的子查询吧。尤其是,内部查询其实可以引用外部查询的数据。看起来简单,其实有点门道。咱们再深入一点聊聊这个话题……
SELECT 里的子查询可以加上 额外的计算列,或者那些依赖其他记录或表的数据。比如,你可以查出学生列表,顺便带上他们的平均分、选了多少门课,或者小组里的当前最高分。这在你想“边查边分析”数据时特别有用,能直接生成汇总列,不用提前处理数据。
SELECT 里子查询的基础
在上例子前,先看看基本语法。SELECT 里的子查询长这样:
SELECT column1,
column2,
(SELECT 聚合_或_条件 FROM 另一个_表 WHERE 条件) AS 新列名
FROM 主表;
注意,子查询返回一个值,这个值会作为新列出现在结果集里。而且 条件 可以引用 主表 的列。
例子1:加上学生的平均分
先来个简单又实用的查询:我们有 students 表,还有 grades 表,里面存着学生的分数。
students 表:
| id | name |
|---|---|
| 1 | Alex Lin |
| 2 | Anna Song |
| 3 | Dan Seth |
grades 表:
| student_id | grade |
|---|---|
| 1 | 90 |
| 1 | 85 |
| 2 | 76 |
| 3 | 88 |
| 3 | 92 |
现在我们想查出学生的名字和他们的平均分。用 SELECT 里的子查询就行:
SELECT
s.id,
s.name,
(SELECT AVG(g.grade)
FROM grades g
WHERE g.student_id = s.id) AS average_grade
FROM students s;
结果:
| id | name | average_grade |
|---|---|---|
| 1 | Alex Lin | 87.5 |
| 2 | Anna Song | 76.0 |
| 3 | Dan Seth | 90.0 |
这里的子查询 (SELECT AVG(g.grade) FROM grades g WHERE g.student_id = s.id) 就是算每个学生的平均分。它对 students 表的每一行都返回一个值,这样你不用 JOIN 或提前建视图也能搞定。
例子2:统计每个学生选了多少门课
现在我们再加点数据:看看每个学生选了多少门课。我们有下面这些表:
enrollments 表:
| student_id | course_id |
|---|---|
| 1 | 101 |
| 1 | 102 |
| 2 | 101 |
查出学生和他们选的课程数:
SELECT
s.id,
s.name,
(SELECT COUNT(*)
FROM enrollments e
WHERE e.student_id = s.id) AS course_count -- 这里引用了外部 students 表
FROM students s;
结果:
| id | name | course_count |
|---|---|---|
| 1 | Alex Lin | 2 |
| 2 | Anna Song | 1 |
| 3 | Dan Seth | 0 |
子查询 (SELECT COUNT(*) FROM enrollments e WHERE e.student_id = s.id) 就是统计 enrollments 表里每个学生的记录数。
子查询里的数据聚合
很多时候,SELECT 里的子查询就是用来算聚合数据的。像 AVG、SUM、COUNT、MAX、MIN 这些函数,都能直接在别的查询里用。
例子3:学生的总分
我们再加一列,每个学生的总分。用子查询把 grades 表里所有分数加起来:
SELECT
s.id,
s.name,
(SELECT SUM(g.grade)
FROM grades g
WHERE g.student_id = s.id) AS total_grade
FROM students s;
结果:
| id | name | total_grade |
|---|---|---|
| 1 | Alex Lin | 175 |
| 2 | Anna Song | 76 |
| 3 | Dan Seth | 180 |
这个子查询 (SELECT SUM(g.grade) FROM grades g WHERE g.student_id = s.id) 就是把每个学生的分数加起来。如果学生没分数,结果就是 NULL,因为 SUM 没有值时会返回 NULL。
限制和建议
- 性能问题。
SELECT里的子查询会对主表的每一行都执行一次。数据量大时,可能会很慢。如果能用JOIN或提前聚合好的数据,尽量用。比如:
SELECT
s.id,
s.name,
g.total_grade
FROM students s
LEFT JOIN (
SELECT student_id, SUM(grade) AS total_grade
FROM grades
GROUP BY student_id
) g ON s.id = g.student_id;
这种 JOIN 的写法更高效,因为聚合和统计只做一次。
2. NULL 的坑。
如果子查询没查到数据,结果就是 NULL。有时候你可能没想到。比如:
SELECT
s.id,
s.name,
(SELECT SUM(g.grade)
FROM grades g
WHERE g.student_id = s.id) AS total_grade
FROM students s;
如果某个学生在 grades 里没有记录,total_grade 就是 NULL。想把 NULL 变成 0,可以用 COALESCE:
SELECT
s.id,
s.name,
COALESCE((SELECT SUM(g.grade)
FROM grades g
WHERE g.student_id = s.id), 0) AS total_grade
FROM students s;
对,这里 COALESCE 的第一个参数就是
(
SELECT SUM(g.grade)
FROM grades g
WHERE g.student_id = s.id
)
优化 SELECT 里的子查询
想让查询更快、少点重复计算,可以这样:
- 给子查询用到的列加索引。比如
grades表的student_id加索引,过滤会快很多。 - 能用
JOIN预先聚合好的数据就用,不要全靠子查询。 - 用
WHERE过滤,尽量减少子查询处理的数据量。
最后一个例子:子查询大集合
咱们把前面学的都用上,写个查询:查出学生名字、平均分、课程数和总分:
SELECT
s.id,
s.name,
(SELECT AVG(g.grade)
FROM grades g
WHERE g.student_id = s.id) AS average_grade,
(SELECT COUNT(*)
FROM enrollments e
WHERE e.student_id = s.id) AS course_count,
(SELECT SUM(g.grade)
FROM grades g
WHERE g.student_id = s.id) AS total_grade
FROM students s;
这个查询直接查出学生的完整档案,全靠子查询的威力。你能看到平均分、总分,还有选了几门课。像这样写,能很快拿到聚合信息,不用建 VIEW,也不用 JOIN。
| id | name | average_grade | course_count | total_grade |
|---|---|---|---|---|
| 1 | Alex Lin | 87.5 | 2 | 175 |
| 2 | Anna Song | 76.0 | 1 | 76 |
| 3 | Dan Seth | 90.0 | 0 | 180 |
GO TO FULL VERSION