在上一章,我们学会了用 JOIN 来横向地“拼接”多张表。这一章,我们要学习一种纵向地“嵌套”查询的强大技术——子查询 (Subquery)。
子查询,顾名思义,就是一个嵌套在另一个 SQL 查询语句内部的 SELECT 语句。它可以让我们的查询逻辑变得像套娃一样,一层包一层,从而解决一些用 JOIN 不太好处理的复杂问题。
一个子查询可以出现在主查询的 SELECT 子句、FROM 子句、或 WHERE 子句中,每种用法都有其独特的应用场景。
准备工作
我们继续使用上一章创建的 users 和 posts 表。如果你的数据不在了,可以重新运行下面的代码:
-- 如果表不存在,则创建
CREATE TABLE IF NOT EXISTS users (
user_id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
);
CREATE TABLE IF NOT EXISTS posts (
post_id INT PRIMARY KEY,
author_id INT,
title VARCHAR(200),
content TEXT,
FOREIGN KEY (author_id) REFERENCES users(user_id)
);
-- 清空并插入数据
TRUNCATE users, posts RESTART IDENTITY CASCADE; -- 清空并重置ID
INSERT INTO users (user_id, username, email) VALUES
(1, '老王', 'wang@example.com'),
(2, '李雷', 'li@example.com'),
(3, '韩梅梅', 'han@example.com');
INSERT INTO posts (post_id, author_id, title, content) VALUES
(101, 1, 'PostgreSQL入门', '内容...'),
(102, 2, 'JOIN的艺术', '内容...'),
(103, 1, '深入理解索引', '内容...');
- RESTART IDENTITY CASCADE: 这是 TRUNCATE 的一个有用选项,它会重置由 SERIAL 等类型管理的自增计数器,并级联清空引用该表的其他表。
12.1 在WHERE子句中使用子查询
这是子查询最常见的用法。WHERE 子句中的子查询通常返回一个值或一个值的列表,用作外部查询的过滤条件。
场景:我们想找出谁写了《JOIN的艺术》这篇文章,然后查询出这个作者的所有信息。
思路分解:
- 第一步 (内部查询): 先从 posts 表中找到标题为《JOIN的art》的文章的 author_id。
- SELECT author_id FROM posts WHERE title = 'JOIN的艺术'; -- 这个查询返回 2
- 第二步 (外部查询): 根据上一步得到的 author_id,去 users 表里查找对应的用户信息。
- SELECT * FROM users WHERE user_id = 2;
用子查询合并为一步:
我们可以把第一步的查询,作为第二步查询的 WHERE 条件!
SELECT *
FROM users
WHERE user_id = (SELECT author_id FROM posts WHERE title = 'JOIN的艺术');
- 括号里的就是子查询。数据库会先执行子查询,得到结果 2。
- 然后,外部查询就变成了 SELECT * FROM users WHERE user_id = 2;。
使用 IN 配合子查询
如果子查询可能返回多行结果,我们就不能用 = 了,而应该用 IN。
场景:找出所有写过文章的用户的详细信息。
SELECT *
FROM users
WHERE user_id IN (SELECT DISTINCT author_id FROM posts);
- 子查询 SELECT DISTINCT author_id FROM posts 会返回一个作者 ID 列表 (1, 2)。
- 外部查询就变成了 SELECT * FROM users WHERE user_id IN (1, 2);。
(这个例子用 JOIN 也能轻松实现,而且通常性能更好。这里只是为了演示 IN 的用法。)
12.2 在FROM子句中使用子查询
当子查询出现在 FROM 子句中时,它的查询结果集会被当作一个临时的、虚拟的表来使用。我们必须给这个临时的表起一个别名 (Alias)。
这种用法非常强大,因为它允许我们先对数据进行一轮复杂的预处理(比如聚合、排序),然后再基于这个处理过的结果进行二次查询。
场景:我们想找出发表文章数量最多的作者的用户名。
思路分解:
- 第一步 (内部查询): 先按 author_id 分组,统计每个作者发表的文章数,并找到数量最多的。
- SELECT author_id, COUNT(*) AS post_count FROM posts GROUP BY author_id;
- 这个查询会得到一个结果集,像一张新表:
- author_id | post_count -----------+------------ 1 | 2 2 | 1
- 第二步 (外部查询): 基于上面这个“虚拟表”,我们可以找到 post_count 最大的那一行,然后和 users 表进行 JOIN 操作,得到用户名。
用子查询合并为一步:
SELECT
u.username,
author_stats.post_count
FROM
users AS u
JOIN
(
SELECT author_id, COUNT(*) AS post_count
FROM posts
GROUP BY author_id
) AS author_stats ON u.user_id = author_stats.author_id
ORDER BY
author_stats.post_count DESC
LIMIT 1;
代码解析 :
- FROM 后面跟着的那个长长的括号里的内容,就是我们的子查询。
- 我们给这个子查询的结果集起了个别名,叫 author_stats。
- 然后,我们就可以像操作一张普通的表一样,把 users 表和这个 author_stats 虚拟表进行 JOIN。
12.3 在SELECT子句中使用子查询(标量子查询)
子查询也可以放在 SELECT 列表中,但它必须是标量子查询 (Scalar Subquery),意思是它必须且只能返回单一的一行、一列的值。
这种用法可以让我们在主查询的每一行结果旁边,附加上一个通过计算得出的额外信息。
场景:在查询用户列表时,顺便显示出每个用户发表的文章总数。
SELECT
u.username,
(
SELECT COUNT(*)
FROM posts AS p
WHERE p.author_id = u.user_id
) AS post_count
FROM
users AS u;
执行过程揭秘:
- 外部查询先从 users 表获取每一行(比如先拿到“老王”这一行)。
- 对于“老王”这一行,u.user_id 的值是 1。
- 然后,内部的子查询被执行,它变成了 SELECT COUNT(*) FROM posts AS p WHERE p.author_id = 1;,返回结果 2。
- 于是,外部查询的第一行结果就是 '老王', 2。
- 接着,外部查询获取下一行“李雷”,重复这个过程…
执行结果:
username | post_count
----------+------------
老王 | 2
李雷 | 1
韩梅梅 | 0
(3 rows)
(这个例子用 LEFT JOIN + GROUP BY 也能实现,性能上通常 JOIN 会更优。但在某些情况下,标量子查询的可读性更高。)
12.4EXISTS和NOT EXISTS
EXISTS 是一个非常独特的运算符,它后面跟着一个子查询。它不关心子查询返回了什么内容,只关心子查询有没有返回任何行。
- 如果子查询返回了至少一行,EXISTS 的结果就为 true。
- 如果子查询返回了零行,EXISTS 的结果就为 false。
场景:找出所有至少写过一篇文章的用户的名字。
SELECT username
FROM users AS u
WHERE EXISTS (
SELECT 1 FROM posts AS p WHERE p.author_id = u.user_id
);
- SELECT 1 这里的 1 没什么特殊含义,可以是 * 或任何列名,因为 EXISTS 只看行数,不看内容。写 1 是一个性能上的小优化。
- 这个查询比用 IN (WHERE user_id IN (SELECT ...) ) 的效率通常要高,尤其是在子查询结果集很大的时候。
NOT EXISTS 则正好相反,用于找出所有没有写过文章的用户。
SELECT username
FROM users AS u
WHERE NOT EXISTS (
SELECT 1 FROM posts AS p WHERE p.author_id = u.user_id
);
这会返回“韩梅梅”。
本章小结
你已经掌握了 SQL 中这项灵活而强大的技术!
- 我们学会了在 WHERE 子句中使用子查询进行条件过滤。
- 学会了在 FROM 子句中使用子查询,将结果作为虚拟表进行二次加工。
- 学会了在 SELECT 子句中使用标量子查询来添加额外信息。
- 还掌握了高效的 EXISTS 和 NOT EXISTS 来检查存在性。
子查询和 JOIN 是解决复杂查询问题的两大支柱。很多时候,同一个问题可以用这两种方式来解决。选择哪一种,取决于可读性、性能和个人偏好。
在下一章,我们将学习一种能让复杂查询(尤其是包含多个子查询的查询)变得更清晰、更易于管理的神器——通用表表达式 (CTE)。准备好让你的 SQL 代码变得像诗一样优雅了吗?我们下一章见!