可锐资源网

技术资源分享平台,提供编程学习、网站建设、脚本开发教程

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

在上一章,我们学会了用 JOIN 来横向地“拼接”多张表。这一章,我们要学习一种纵向地“嵌套”查询的强大技术——子查询 (Subquery)


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

一个子查询可以出现在主查询的 SELECT 子句、FROM 子句、或 WHERE 子句中,每种用法都有其独特的应用场景。


准备工作

我们继续使用上一章创建的 usersposts 表。如果你的数据不在了,可以重新运行下面的代码:

-- 如果表不存在,则创建
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的艺术》这篇文章,然后查询出这个作者的所有信息。

思路分解:

  1. 第一步 (内部查询): 先从 posts 表中找到标题为《JOIN的art》的文章的 author_id
  2. SELECT author_id FROM posts WHERE title = 'JOIN的艺术'; -- 这个查询返回 2
  3. 第二步 (外部查询): 根据上一步得到的 author_id,去 users 表里查找对应的用户信息。
  4. 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)

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

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

思路分解:

  1. 第一步 (内部查询): 先按 author_id 分组,统计每个作者发表的文章数,并找到数量最多的。
  2. SELECT author_id, COUNT(*) AS post_count FROM posts GROUP BY author_id;
  3. 这个查询会得到一个结果集,像一张新表:
  4. author_id | post_count -----------+------------ 1 | 2 2 | 1
  5. 第二步 (外部查询): 基于上面这个“虚拟表”,我们可以找到 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 子句中使用标量子查询来添加额外信息。
  • 还掌握了高效的 EXISTSNOT EXISTS 来检查存在性

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

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

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言