第 12 章:子查询 (Subquery) - PostgreSQL入门

B站影视 港台电影 2025-08-05 07:37 1

摘要:子查询,顾名思义,就是一个嵌套在另一个 SQL 查询语句内部的 SELECT 语句。它可以让我们的查询逻辑变得像套娃一样,一层包一层,从而解决一些用 JOIN 不太好处理的复杂问题。

在上一章,我们学会了用 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; -- 清空并重置IDINSERT 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 等类型管理的自增计数器,并级联清空引用该表的其他表。

这是子查询最常见的用法。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 usersWHERE user_id = (SELECT author_id FROM posts WHERE title = 'JOIN的艺术');括号里的就是子查询。数据库会先执行子查询,得到结果 2。然后,外部查询就变成了 SELECT * FROM users WHERE user_id = 2;。

使用 IN 配合子查询
如果子查询可能返回多行结果,我们就不能用 = 了,而应该用 IN。

场景:找出所有写过文章的用户的详细信息。

SELECT *FROM usersWHERE 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);。

这种用法非常强大,因为它允许我们先对数据进行一轮复杂的预处理(比如聚合、排序),然后再基于这个处理过的结果进行二次查询。

场景:我们想找出发表文章数量最多的作者的用户名。

思路分解:

第一步 (内部查询): 先按 author_id 分组,统计每个作者发表的文章数,并找到数量最多的。SELECT author_id, COUNT(*) AS post_count FROM posts GROUPBY author_id;这个查询会得到一个结果集,像一张新表: author_id | post_count + 1 | 2 2 | 1第二步 (外部查询): 基于上面这个“虚拟表”,我们可以找到 post_count 最大的那一行,然后和 users 表进行 JOIN 操作,得到用户名。

用子查询合并为一步:

SELECT u.username, author_stats.post_countFROM users AS uJOIN ( SELECT author_id, COUNT(*) AS post_count FROM posts GROUP BY author_id ) AS author_stats ON u.user_id = author_stats.author_idORDER BY author_stats.post_count DESCLIMIT 1;

代码解析 :

FROM 后面跟着的那个长长的括号里的内容,就是我们的子查询。我们给这个子查询的结果集起了个别名,叫 author_stats。然后,我们就可以像操作一张普通的表一样,把 users 表和这个 author_stats 虚拟表进行 JOIN。

子查询也可以放在 SELECT 列表中,但它必须是标量子查询 (Scalar Subquery),意思是它必须且只能返回单一的一行、一列的值。

这种用法可以让我们在主查询的每一行结果旁边,附加上一个通过计算得出的额外信息。

场景:在查询用户列表时,顺便显示出每个用户发表的文章总数。

SELECT u.username, ( SELECT COUNT(*) FROM posts AS p WHERE p.author_id = u.user_id ) AS post_countFROM 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)

EXISTS 是一个非常独特的运算符,它后面跟着一个子查询。它不关心子查询返回了什么内容,只关心子查询有没有返回任何行

如果子查询返回了至少一行,EXISTS 的结果就为 true。如果子查询返回了零行,EXISTS 的结果就为 false。

场景:找出所有至少写过一篇文章的用户的名字。

SELECT usernameFROM users AS uWHERE 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 usernameFROM users AS uWHERE NOT EXISTS ( SELECT 1 FROM posts AS p WHERE p.author_id = u.user_id);

这会返回“韩梅梅”。

你已经掌握了 SQL 中这项灵活而强大的技术!

子查询和 JOIN 是解决复杂查询问题的两大支柱。很多时候,同一个问题可以用这两种方式来解决。选择哪一种,取决于可读性、性能和个人偏好。

在下一章,我们将学习一种能让复杂查询(尤其是包含多个子查询的查询)变得更清晰、更易于管理的神器——通用表表达式 (CTE)。准备好让你的 SQL 代码变得像诗一样优雅了吗?我们下一章见!

来源:linux运维菜

相关推荐