SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

  1. 首页
  2. 博客
  3. 数据库和 SQL

数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

子查询是另一个查询中的一个查询。了解每种类型及其适合的位置,正确理解相关子查询,并避免默默地不返回任何行的 NOT IN 陷阱。

阅读约 9 分钟
SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

要点速览

  • 子查询是嵌套在另一个查询中的查询,外部查询使用其结果。
  • 子查询可以出现在WHERE、FROM、SELECT和HAVING中。
  • 相关子查询引用外部行,因此它在逻辑上是按行计算的。
  • 当可能存在 NULL 时,优先选择 NOT EXISTS 而不是 NOT IN。

有些问题需要两个步骤才能回答。 “哪些订单高于平均水平?”要求你先求平均值,然后将每个订单与它进行比较。 SQL 子查询 允许您将这两个步骤编写为一条语句。

本教程使用两个小表涵盖每种类型。我们在 SQLite 3.50 中运行这些查询,显示的结果来自该运行。标准子查询语法得到广泛支持,我们指出了数据库不同的地方。

什么是子查询?

子查询也称为内部查询或嵌套查询,是放置在另一个语句内的 SELECT 语句,位于圆括号中。从概念上讲,内部查询生成外部查询然后使用的结果。

SELECT id, total
FROM orders
WHERE total > (SELECT AVG(total) FROM orders);

样本表

CREATE TABLE customers (id INT, name TEXT, city TEXT);
CREATE TABLE orders (id INT, customer_id INT, total INT);

INSERT INTO customers VALUES
 (1,'Asha','Pune'), (2,'Ben','Delhi'), (3,'Chitra','Pune'), (4,'Dev','Goa');

INSERT INTO orders VALUES
 (101,1,1200), (102,1,800), (103,2,300), (104,3,2500), (105,3,100);

请注意,Dev 没有订单。这个细节对于下面的几个例子很重要。

子查询类型一览

类型 退货 典型位置 常用运算符
标量 一个值 WHERE, SELECT =, >, <
多排 一列,多行 哪里 IN、ANY、ALL
表(派生) 许多列和行 从 像桌子一样使用
相关 取决于外排 WHERE, SELECT 任意,并且 EXISTS

标量子查询

标量子查询恰好返回一个值,因此您可以在单个值有意义的任何地方使用它。这是之前的查询及其结果。所有订单的平均值为 980:

SELECT id, total
FROM orders
WHERE total > (SELECT AVG(total) FROM orders);

-- 101 | 1200
-- 104 | 2500

您不能编写 WHERE total > AVG(total),因为不允许直接在 WHERE 中进行聚合。子查询通过单独计算值来解决这个问题。

如果“标量”子查询返回多行怎么办?大多数数据库,包括 PostgreSQL、MySQL 和 SQL Server,都会引发错误。 SQLite 没有。在我们的测试中,它仅默默地使用第一行,这可能隐藏真正的错误,因此不要依赖此行为。

在和不在

当内部查询返回列表时,使用IN。这会找到至少一个订单超过 1000 的客户:

SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders WHERE total > 1000);

-- Asha
-- Chitra

NOT IN 则相反,查找从未订购过的客户:

SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM orders);

-- Dev

这在这里可行,但是 NOT IN 有一个危险的边缘情况,我们将在下面介绍。

FROM 中的子查询

FROM 中的子查询充当临时表,通常称为派生表。当您需要聚合两次时,它非常有用,例如在每个城市中每个客户的平均支出的查询中:

SELECT city, AVG(spent) AS avg_spent
FROM (
  SELECT c.city, SUM(o.total) AS spent
  FROM customers c
  JOIN orders o ON o.customer_id = c.id
  GROUP BY c.id, c.city
) AS per_customer
GROUP BY city;

-- Delhi |  300.0
-- Pune  | 2300.0

内部查询对每个客户进行总计,外部查询对这些总计进行平均。为派生表提供别名是一个好习惯,因为某些数据库需要它。如果 GROUP BY 对您来说是新的,请参阅 SQL GROUP BY 和 HAVING。

SELECT 中的子查询

标量子查询还可以添加计算列。这个计算每个客户的订单:

SELECT name,
       (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS order_count
FROM customers c;

-- Asha   | 2
-- Ben    | 1
-- Chitra | 2
-- Dev    | 0

Dev 显示为零。内部联接会完全删除 Dev,因为 Dev 没有匹配的行。使用 LEFT JOIN 和 GROUP BY 可以获得相同的结果,我们在 SQL 连接解释 中对此进行了解释。

相关子查询

前面的示例也是相关的,因为内部查询引用外部查询中的 c.id。非相关子查询可以单独运行。相关的行不能,因为它取决于当前外行的值。

这会发现订单高于同一客户的平均水平:

SELECT o.id, o.customer_id, o.total
FROM orders o
WHERE o.total > (
  SELECT AVG(i.total)
  FROM orders i
  WHERE i.customer_id = o.customer_id
);

-- 101 | 1 | 1200
-- 104 | 3 | 2500

以下是阅读方法。对于订单 101,内部查询对客户 1 的订单(1200 和 800)进行平均,得到 1000。由于 1200 更大,因此保留该行。对于订单 102,适用相同的平均值,但 800 并不更大,因此被删除。客户2的单笔订单等于其自身的平均订单,因此也被丢弃。

特点 不相关 相关
使用外部查询值? 否 是的
可以自己运行吗? 是的 否
逻辑执行 一次 每外排一次
绩效风险 通常较低 可以放在大桌子上

“每行一次”描述了逻辑,查询优化器通常将它们重写为更有效的东西。尽管如此,缓慢的相关子查询还是值得研究的。内部列上的索引(此处为 orders.customer_id)可以产生重大影响。请参阅SQL 索引解释。

存在与不存在

EXISTS 问一个是或否的问题:内部查询是否至少返回一行?它可以在第一个匹配处停止,并且你在里面选择什么并不重要,这就是为什么人们经常写SELECT 1。

SELECT c.name FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- Asha, Ben, Chitra

SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

-- Dev

NOT IN NULL 陷阱

这是最常见的子查询错误之一。我们创建了一个包含一个真实 ID 和一个 NULL 的表,然后针对它运行两种形式:

CREATE TABLE r (customer_id INT);
INSERT INTO r VALUES (1), (NULL);

SELECT name FROM customers
WHERE id NOT IN (SELECT customer_id FROM r);
-- (no rows at all)

SELECT c.name FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM r WHERE r.customer_id = c.id);
-- Ben, Chitra, Dev

为什么第一个不返回任何内容? id NOT IN (1, NULL) 表示 id <> 1 AND id <> NULL。与 NULL 的任何比较都是未知的而不是真实的,因此整个条件永远不可能为真。没有错误消息,也没有警告,只是一个看起来合理的空结果。

提示:默认使用NOT EXISTS进行反连接。如果您确实使用 NOT IN,请在子查询中添加 WHERE customer_id IS NOT NULL。

子查询、JOIN、CTE

许多查询可以用多种方式编写。早些时候,我们使用IN找到了大订单的客户。这是使用连接得到的相同结果:

SELECT DISTINCT c.name FROM customers c
JOIN orders o ON o.customer_id = c.id AND o.total > 1000;

-- Asha
-- Chitra

请注意DISTINCT。如果客户有多个匹配项,则联接可以产生重复项,而 IN 和 EXISTS 则不能。

使用 当
加入 您需要两个表中的列
在或存在 您只需根据是否存在匹配项进行过滤
派生表 你需要先聚合,而且时间很短
热膨胀系数 该逻辑有多个步骤,或者多次使用
窗函数 每行旁边都需要一个每组的数字

性能因数据库和数据而异,现代优化器通常会生成类似的计划。因此,首先选择最清晰的形式,然后检查EXPLAIN是否发现它很慢。

常见错误

  • 对多行结果使用 =。 使用 IN,或确保内部查询返回一行。
  • 在可为空的列上使用NOT IN。您可能会默默地丢失每一行。
  • 使用不明确的别名。 在相关查询中,限定每一列,否则它可能绑定到错误的表。
  • 深度嵌套。 三个级别很难阅读,因此将它们转换为 CTE。
  • 内部添加ORDER BY。通常对外部结果没有影响,有些数据库拒绝它。

最后的话

子查询是表达两步问题的自然方式。从标量子查询和 IN 开始,然后通过一次推理单个外部行来练习相关子查询。只要可能出现 NULL,请记住 NOT EXISTS。为了练习,用三种方式编写一个问题,即子查询、连接和 CTE,然后比较结果和可读性。

常见问题

SQL中什么是子查询?

子查询是嵌套在另一个 SQL 语句内(用方括号括起来)的 SELECT 语句。外部语句将其结果用作值、列表或表格。

什么是相关子查询?

它是引用外部查询中的列的子查询。它依赖于当前的外层行,因此它不能单独运行。

子查询和 JOIN 哪个更快?

这取决于数据库和数据,优化器通常会产生类似的计划。首先编写最清晰的版本,如果性能很重要,然后比较执行计划。

IN 和 EXISTS 和有什么区别?

IN 将一个值与结果列表进行比较。 EXISTS 检查是否有任何行被返回。当被否定时,它们与 NULL 的行为不同。

子查询可以返回多列吗?

是的,当在 FROM 中用作派生表时,某些数据库也允许行比较。标量子查询必须仅返回一列和一行。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件测试

用户验收测试 (UAT):流程、清单和真实示例

用户验收测试解释:谁运行它、它与 QA 有何不同、七步流程、UAT 测试用例模板、清单、签核标准和常见错误。

阅读文章 →
数据库与 SQL

SQL NULL 值解释:IS NULL、COALESCE、NULLIF 和要避免的陷阱

通过我们运行的查询解释了 SQL NULL 值:三值逻辑、IS NULL、COALESCE、NULLIF,聚合、连接和排序如何处理 NULL,以及它悄悄引起的错误。

阅读文章 →
数据库与 SQL

SQL CASE WHEN 语句:语法、实例和常见错误

SQL CASE WHEN 解释了我们运行的查询:简单和搜索表单、条件顺序、SUM 透视、COUNT 技巧、自定义排序、更新和要避免的 NULL 陷阱。

阅读文章 →
软件开发

什么是智威汤逊?通过工作示例解释 JSON Web 令牌

什么是智威汤逊? JSON Web Tokens 解释:这三个部分,在我们运行的代码中构建和篡改的令牌,JWT 与会话,存储,刷新令牌和安全错误。

阅读文章 →
软件开发

JavaScript 事件循环解释:调用堆栈、微任务和计时器以及示例

JavaScript 事件循环用我们运行的代码进行了解释:调用堆栈、任务和微任务队列、为什么 setTimeout 0 不是立即执行、阻塞、饥饿和面试测验。

阅读文章 →
软件开发

Python 生成器和 Yield 解释:通过真实示例节省内存

Python 生成器和yield 用我们运行的代码进行了解释:它们如何暂停和恢复、测量内存节省、管道、无限序列、yield from、发送和常见陷阱。

阅读文章 →
人工智能

什么是嵌入?向量和余弦相似度简单解释

什么是嵌入?关于向量、余弦相似度和语义搜索的简单语言指南,其中包含我们运行的一个小示例、常见用途、选择模型和已知限制。

阅读文章 →
DevOps

蓝绿部署与金丝雀部署:差异、权衡以及如何选择

蓝绿部署与金丝雀部署解释:每种部署的工作原理、成本、风险和回滚、我们测试的稳定用户分桶、数据库迁移、滚动更新以及如何选择。

阅读文章 →
测试自动化

不稳定的测试:造成这些问题的原因以及如何彻底解决它们

我们运行的演示解释了不稳定的测试:主要原因、如何确认和诊断、正确的时间修复、共享状态和日期以及隔离过程。

阅读文章 →
软件测试

负载测试与压力测试:差异、指标和 k6 示例

负载测试与压力测试的解释:各自证明什么、峰值和浸泡测试、重要的指标、为什么平均值会误导、k6 个示例以及如何计划测试。

阅读文章 →
软件测试

测试用例与测试场景:主要区别、示例以及何时使用它们

测试用例与测试场景的解释:清晰的定义、并排的表格、从场景到案例的密码重置功能、模板以及何时使用每个功能。

阅读文章 →
数据库与 SQL

SQL 查询优化:读取执行计划并修复慢速查询(带计时)

对一百万行进行 SQL 查询优化测试:读取执行计划、添加正确的索引、修复不可控制的过滤器、使用复合索引和键集分页。

阅读文章 →
数据库与 SQL

SQL UNION 与 UNION ALL:差异、示例和性能测试

SQL UNION 与 UNION ALL 通过我们运行的查询进行了解释:重复、都遵循的规则、真实的数据错误、测量的性能、INTERSECT 和 EXCEPT 以及何时使用它们。

阅读文章 →
软件开发

JavaScript Fetch API 教程:GET、POST、错误处理和超时

针对本地服务器测试的 JavaScript Fetch API 教程:GET 和 POST、为什么不抛出 404、可重用的帮助程序、查询字符串、超时、CORS 和常见错误。

阅读文章 →
软件开发

Python 文件处理:读取、写入、CSV、JSON 和 pathlib 以及示例

使用我们运行的代码解释了 Python 文件处理:打开模式、使用、读取大文件、CSV、JSON、pathlib、二进制文件、安全写入、错误处理和常见错误。

阅读文章 →
软件开发

Python 类和对象:带有实际示例的 OOP 教程

用我们运行的代码解释 Python 类和对象:__init__、方法、类与实例属性、继承、属性、dunder 方法、数据类和常见错误。

阅读文章 →
人工智能

什么是大语言模型?法学硕士如何运作,在没有炒作的情况下进行解释

什么是大语言模型?法学硕士用简单的语言解释:标记、嵌入、注意力、训练、温度和上下文窗口、一个微小的下一个单词演示和真正的限制。

阅读文章 →
DevOps

适合初学者的 Linux 命令:带有示例的基本终端指南

适合初学者的 Linux 命令,按任务分组:导航、文件、查看日志、grep、管道、权限、进程和档案,并附有实用的日志分析示例。

阅读文章 →
测试自动化

Selenium 与 Playwright:主要区别、代码比较以及选择

Selenium 与 Playwright 比较:架构、等待、浏览器和语言支持、调试、两种工具中的相同测试、迁移建议以及如何选择。

阅读文章 →
软件测试

等价划分:通过实例解释测试设计技术

等效分区解释:如何找到有效和无效分区,年龄、密码和优惠券的工作示例,我们运行的测试,以及为什么将其与边界配对。

阅读文章 →
软件测试

软件测试中的严重性与优先级:差异、矩阵和实例

软件测试中的严重性与优先级解释:清晰的定义、带有示例的四向矩阵、级别尺度、谁设置每个级别以及如何运行错误分类。

阅读文章 →
数据库与 SQL

SQL 子查询教程:类型、相关子查询和 EXISTS 以及示例

SQL 子查询教程,其中包含我们运行的查询:标量、IN、FROM 和 SELECT 子查询、相关子查询、EXISTS、NOT IN NULL 陷阱以及子查询与 JOIN 与 CTE。

阅读文章 →
数据库与 SQL

SQL 事务和 ACID 解释:COMMIT、ROLLBACK 和隔离级别

SQL 事务和 ACID 通过我们运行的银行转账进行了解释:COMMIT、ROLLBACK、保存点、锁、隔离级别、脏读、死锁和最佳实践。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →