SQL 深度解析:NOT IN 与 NOT EXISTS 的本质区别、性能陷阱与最佳实践

作为一名长期与数据库打交道的开发者,我发现编写 SQL 查询语句虽然看似简单,但要做到既高效又准确,往往需要对底层机制有深刻的理解。在编写涉及数据过滤的 SQL 语句时,我们经常需要在 INLINECODE9b89baec 和 INLINECODE77a72353 之间做出选择。这两个运算符表面上看都用于排除数据,但它们在处理 NULL 值、执行逻辑以及性能表现上有着截然不同的特性。

很多时候,开发者因为忽视这些细微的差别,导致查询结果出错或性能急剧下降。别担心,在今天的这篇文章中,我们将深入探讨 INLINECODE87ec495d 和 INLINECODE9b3493dc 的方方面面。我们将通过具体的实例,分析它们的工作原理、处理 NULL 值的策略,以及在实际业务场景中如何做出正确的选择,帮助你避开那些常见的“坑”。

为了更直观地演示,让我们先在数据库中创建两个演示表。这两个表将贯穿我们今天的所有示例:一个是用户信息表 INLINECODE9ed29259,另一个是课程表 INLINECODE49402c99。

准备工作:构建演示环境

首先,我们需要建立数据环境。执行以下 SQL 语句来创建表结构并插入初始数据。

1. 创建用户信息表 (user_info)

这个表存储了用户的基本信息,包括 ID、姓名和总分。

-- 创建 user_info 表
CREATE TABLE user_info(
  id int PRIMARY KEY,
  name varchar(100),
  total_score int
 );

-- 插入测试数据
INSERT INTO user_info(id,name,total_score)
VALUES(01,‘Vishu‘,100);

INSERT INTO user_info(id,name,total_score)
VALUES(02,‘Neeraj‘,95);

INSERT INTO user_info(id,name,total_score)
VALUES(03,‘Aayush‘,85);

INSERT INTO user_info(id,name,total_score)
VALUES(04,‘Vivek‘,70);
``

### 2. 创建课程表 (courses)

这个表记录了用户选修的课程。注意这里 `id` 字段是指向 `user_info` 的外键,且一个用户可以选多门课。

sql

— 创建 courses 表

CREATE TABLE courses(

id int,

course varchar(100)

);

— 插入测试数据:注意 ID 01 选了两门课

INSERT INTO courses(id, course) VALUES(01,‘Python‘);

INSERT INTO courses(id, course) VALUES(02,‘Python‘);

INSERT INTO courses(id, course) VALUES(01,‘Java‘);

INSERT INTO courses(id, course) VALUES(04, ‘JavaScript‘);

— 稍后我们会用到 DSA 课程,但目前先不插入


好了,数据环境已经准备就绪。现在,让我们开始深入探讨这两个运算符。

## 深入理解 NOT IN 运算符

`NOT IN` 是 SQL 中非常直观的逻辑运算符。从字面意思上我们就能理解,它是用于筛选出“不在指定列表中”的数据。当我们想要排除一组特定的值时,它通常是我们的首选,因为写法非常简单直接。

### 基本语法

sql

SELECT column01, column02,…….

FROM table_name

WHERE column_name NOT IN ( value1, value2, … );


### 实战示例:基本用法

让我们先看一个最简单的例子。假设我们想要查询 `user_info` 表中所有 ID **不是** 2 和 4 的用户。

**查询语句:**

sql

SELECT *

FROM user_info

WHERE id NOT IN (2, 4);


**执行结果分析:**
数据库引擎会检查 `user_info` 中的每一行。对于每一行,它会判断 `id` 是否等于 2 或者 4。如果都不等于,这行就被保留下来。因此,结果将是 ID 为 01 和 03 的用户数据。

### 实战示例:在子查询中使用

在实际业务中,我们更常用的是将 `NOT IN` 用于子查询。例如,找出**没有选修 Python 课程**的用户。

**查询语句:**

sql

SELECT name

FROM user_info

WHERE id NOT IN (

SELECT id

FROM courses

WHERE course = ‘Python‘

);


**工作原理:**
1. 子查询 `(SELECT id FROM courses WHERE course = ‘Python‘)` 首先执行,返回选修了 Python 课程的用户 ID 列表(这里是 01 和 02)。
2. 外层查询接着检查 `user_info` 表中的每一个用户。
3. 如果用户的 ID 不在这个列表中,就会显示出来。

**预期结果:**
ID 为 03 (Aayush) 和 04 (Vivek) 的用户。等等,Vivek (ID 04) 选了 JavaScript,没选 Python,所以他应该显示;Aayush (ID 03) 没有任何课程记录,也应该显示。这看起来没问题,对吧?

### 陷阱:NOT IN 与 NULL 值的致命交互

这是 `NOT IN` 最危险的地方,也是许多开发者容易忽视的盲点。SQL 使用的是三值逻辑:TRUE、FALSE 和 **UNKNOWN**。

如果 `NOT IN` 列表中的子查询返回了 `NULL` 值,整个查询的结果就会变成“空集”,没有任何数据会被返回!这是因为在 SQL 中,`NULL != 1` 的结果不是 TRUE,而是 `UNKNOWN`。而 `WHERE` 子句只接受结果为 TRUE 的行。

**举个例子,让我们假设 `courses` 表中有一条数据,其 `course` 为 NULL,或者 `id` 为 NULL:**

sql

— 为了演示,我们插入一条 ID 为 NULL 的记录

INSERT INTO courses(id, course) VALUES(NULL, ‘DSA‘);

— 再次执行刚才的查询

SELECT name

FROM user_info

WHERE id NOT IN (

SELECT id

FROM courses

— 这里假设我们查所有课程,包含了那个 NULL ID

);


在这个例子中,由于子查询列表中包含 `NULL`,对于任何一行外层表的数据,`id NOT IN (..., NULL, ...)` 的比较结果都会是 `UNKNOWN`。因此,**你将得不到任何结果**,即使明明有用户没有选修课程。

**解决方案:** 如果你必须使用 `NOT IN`,请务必确保子查询中的列是 `NOT NULL` 的,或者手动在子查询中加上 `IS NOT NULL` 过滤条件:

sql

— 安全的 NOT IN 写法

SELECT name

FROM user_info

WHERE id NOT IN (

SELECT id

FROM courses

WHERE id IS NOT NULL — 关键:过滤掉 NULL 值

);


## 深入理解 NOT EXISTS 运算符

接下来,让我们看看 `NOT EXISTS`。与 `NOT IN` 比较值不同,`NOT EXISTS` 关心的是“满足条件的行是否存在”。它是一个布尔运算符,只返回 TRUE 或 FALSE(不关心 UNKNOWN,除非处理 NULL 本身)。

### 基本语法

sql

SELECT column01, column02,……….

FROM tablename outertable

WHERE NOT EXISTS (

SELECT 1

FROM inner_table

WHERE innertable.id = outertable.id

);


**注意:** 在 `EXISTS` 的子查询中,`SELECT 1` 是一种常见的写法习惯,实际上 `SELECT *` 也可以,因为数据库引擎只关心有没有行返回,而不关心具体返回了什么列。`SELECT 1` 稍微节省了一点点内存开销,但这通常不是性能瓶颈。

### 实战示例:关联子查询

让我们用 `NOT EXISTS` 来完成同样的任务:找出**没有选修 Python 课程**的用户。

**查询语句:**

sql

SELECT id, name

FROM user_info u

WHERE NOT EXISTS (

SELECT 1

FROM courses c

WHERE c.id = u.id

AND c.course = ‘Python‘

);


**工作原理(相关子查询):**
这里发生了所谓的“循环嵌套”逻辑(虽然优化器可能会改变执行计划):
1. 数据库从 `user_info`(别名为 u)中取出第一行用户(例如 ID 01, Vishu)。
2. 进入子查询,检查 `courses` 表中是否存在一行数据,其 `id` 等于 01 **且** `course` 为 ‘Python‘。
3. 如果子查询找到了这样一行,`EXISTS` 返回 TRUE,那么 `NOT EXISTS` 就是 FALSE,Vishu 被过滤掉。
4. 如果子查询没找到,`NOT EXISTS` 返回 TRUE,Vishu 被保留。
5. 重复上述过程直到处理完所有用户。

### 为什么 NOT EXISTS 对 NULL 更安全?

这是 `NOT EXISTS` 最大的优势。当执行 `c.id = u.id` 比较时,如果 `c.id` 是 NULL,这个等式不成立。但这只是意味着那一行不匹配。子查询会继续检查下一行。只要找不到任何匹配的行,`NOT EXISTS` 就是 TRUE。

它不依赖于具体的值列表,而是基于行的“存在性”。因此,子查询中出现 NULL 值不会导致整个外层查询“崩溃”或返回空集。

### 进阶示例:找出没有选修任何课程的用户

让我们来看另一个场景:找出完全没有任何课程记录的用户。

**查询语句:**

sql

SELECT name

FROM user_info u

WHERE NOT EXISTS (

SELECT 1

FROM courses c

WHERE c.id = u.id

);


这个查询非常健壮。即使 `courses` 表里有 `id` 为 NULL 的脏数据,只要那个 NULL 不等于 `u.id`,逻辑就成立。在我们的示例数据中,只有 ID 为 03 (Aayush) 的用户会被返回,因为他没有任何课程记录。

## 核心对比:NOT IN vs NOT EXISTS

既然我们已经了解了它们的用法,让我们通过一个详细的对比表来总结一下它们的关键区别。这将帮助你在实际开发中做出正确的决策。


| 特性 | NOT IN 运算符 | NOT EXISTS 运算符 |
| :--- | :--- | :--- |
| **底层逻辑** | **值比较**:类似于 `!=` 和 `AND` 的组合。它检查一个值是否**不等于**列表中的任何一个。 | **存在性检查**:使用关联逻辑检查子查询是否返回**任何行**。不比较值的具体内容,只看行是否存在。 |
| **NULL 处理** | **极高风险**:如果子查询列表中包含 NULL,整个表达式的逻辑结果会变成 `UNKNOWN`,导致外层查询**不返回任何数据**(这是最常见的 BUG 来源)。 | **安全**:NULL 值通常不影响逻辑,因为 `NULL = ID` 不会导致行匹配。只有当存在匹配行时才返回 FALSE。 |
| **性能表现** | **多变**:在处理短列表时性能尚可。但在处理长列表或非关联子查询时,数据库往往无法有效利用索引。 | **通常更优**:特别是在关联子查询中,一旦子查询找到第一条匹配记录,数据库就可以立即停止扫描(短路特性),利用索引效率极高。 |
| **语法风格** | 简单明了,适合硬编码的值列表 (如 `WHERE id NOT IN (1, 2, 3)`)。 | 稍显复杂,通常必须使用**关联子查询**(Correlated Subquery),即子查询中要引用外层表的字段。 |
| **典型应用** | 1. 硬编码的排除列表。
2. 确定子查询字段非空的数据过滤。 | 1. 寻找“缺失”的数据(例如:没有下单的客户)。
2. 复杂的关联数据校验。
3. 数据可能包含 NULL 的场景。 | ## 性能优化建议与最佳实践 当你面临选择时,建议遵循以下实战经验: ### 1. 性能考量:谁更快? 在旧版本的数据库(如 Oracle 8i 或更早,或早期的 SQL Server)中,`NOT IN` 经常因为糟糕的执行计划而被诟病。但在现代数据库中,对于简单的内部子查询,优化器通常能将 `NOT IN` 和 `NOT EXISTS` 转换为相同的执行计划(通常是 Anti-Join,即反连接)。 **然而,在以下情况中,`NOT EXISTS` 通常胜出:** * **大数据量**:当子查询涉及大量数据时,`NOT EXISTS` 配合索引只需找到第一条匹配项就能停止,而 `NOT IN`(在某些执行计划下)可能需要扫描整个结果列表来进行哈希匹配。 * **复杂索引**:`NOT EXISTS` 允许你在子查询的连接条件上利用索引,这非常灵活。 ### 2. 代码的可读性与安全性 虽然 `NOT IN` 写起来很短,但考虑到 NULL 带来的隐患,**`NOT EXISTS` 是更安全、更符合集合论思维的选择**。 **开发者的黄金法则:** > 除非你非常确定你的列表中不包含 NULL,或者你手动添加了 `IS NOT NULL` 过滤,否则**始终优先使用 `NOT EXISTS`**。这可以防止在生产环境中因为脏数据导致业务逻辑突然中断。 ### 3. 实际场景模拟 **场景 A:** 你需要筛选出状态不是 ‘Cancelled‘ 和 ‘Pending‘ 的订单。 * **推荐:** `NOT IN`。因为 `status` 列通常是枚举型,且不含 NULL。

sql

SELECT * FROM orders WHERE status NOT IN (‘Cancelled‘, ‘Pending‘);


**场景 B:** 你需要找出没有在 `logins` 表中出现过的用户(即从未登录过的用户)。`logins` 表的 `user_id` 可能因为历史原因包含 NULL。
*   **推荐:** `NOT EXISTS`。这能保证即使 `logins` 表有脏数据,你的查询也不会崩溃。
    

sql

SELECT * FROM users u

WHERE NOT EXISTS (SELECT 1 FROM logins l WHERE l.user_id = u.id);

“INLINECODEf790af74NOT ININLINECODE528a5642NOT EXISTSINLINECODE62288fe2NOT ININLINECODEa5f8b05fNOT EXISTSINLINECODEcb051917NOT EXISTSINLINECODEb5b3f825NOT EXISTS

希望这篇文章能帮助你更好地掌握 SQL 查询技巧。如果你在项目中遇到了棘手的查询性能问题,不妨检查一下是否误用了 NOT IN`。编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/47440.html
点赞
0.00 平均评分 (0% 分数) - 0