作为一名长期与数据库打交道的开发者,我发现编写 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`。编码愉快!