IN 是「等于其中任意一个」,等价于一串 OR;EXISTS 是「有没有行」。
NOT IN 的子查询里只要出现一个 NULL,整条语句静默返回空集——这是 SQL 里最坑的一处。
> ANY 等价于 > MIN,> ALL 等价于 > MAX;
= ANY 就是 IN,<> ALL 就是 NOT IN。
9.1a 讲的是「子查询长什么样」,这一节讲「用什么运算符把它接进来」。四个运算符,
各有各的语义,也各有各的坑。
教材 9.1 的后四小节就是这四个。其中 NOT IN 与 NULL 那一段是全文最该动手验一次的。
演示里除了跑语义,还做了一次对比计时:20 万行大表分别用 IN、EXISTS、
JOIN 去对一张 3 行小表。结果和很多老文章说的不一样,原因写在实测那一段。
一、IN:等于其中任意一个
id IN (5, 8, 13) 就是 id = 5 OR id = 8 OR id = 13。
子查询版就是把那一串值换成一条查询的结果。
SELECT sname FROM tb_student
WHERE id IN (SELECT student_id FROM tb_score WHERE subject = '高等数学')
语义上它问的是「这个值在不在那一列里」。 所以能一句话说清「在里面」的时候,
用 IN 比 EXISTS 更贴人话。
子查询返回的必须是一列。 IN (SELECT a, b FROM ...) 会报错——
IN 只跟一个值比,多一列它不知道比哪个。
二、NOT IN 的 NULL 陷阱
这一段请务必自己跑一遍,光看结论记不住。
-- 正常情况:没有成绩的学生被挑出来
SELECT sname FROM tb_student
WHERE id NOT IN (SELECT student_id FROM tb_score) -- 赵敏
-- 子查询里混进一个 NULL,结果立刻变空
SELECT sname FROM tb_student
WHERE id NOT IN (SELECT student_id FROM tb_score
UNION ALL SELECT NULL) -- 0 行
为什么会这样? NOT IN 会被展开成 id <> a AND id <> b AND id <> NULL。
而任何值和 NULL 比,结果都是「未知」。 「未知」再参与 AND,整条就是未知。
WHERE 只留「真」,未知一律丢掉——所以整张表都过不去。
不报错、不警告,就是干干净净的 0 行。
两条出路: 子查询里加 WHERE student_id IS NOT NULL,或者干脆换成 NOT EXISTS。
后者更省心,因为它压根不是「比大小」的语义,不受 NULL 影响。
三、EXISTS:只问有没有
EXISTS 只关心一件事:这个子查询能不能捞出至少一行。 捞到就是真,捞不到就是假。
SELECT sname FROM tb_student st WHERE EXISTS
(SELECT 1 FROM tb_score sc WHERE sc.student_id = st.id)
它不看 SELECT 后面写了什么。 演示里注册了一个「一被调用就抛错」的函数塞进去,
结果一条错都没报——EXISTS 根本没求值那个表达式。
这也解释了为什么 EXISTS (SELECT *) 和 EXISTS (SELECT 1) 一样快。
老文章里说「SELECT * 在 EXISTS 里更慢」是错的——它压根不读。
EXISTS 天然是相关子查询(要跟外层挂上钩),但它的执行方式是「捞到一行就停」,
所以在「只要判断有没有」的场景里,它往往比 IN 更快,因为它不用把整列算完。
四、ANY/SOME 与 ALL
ANY 是「比其中任意一个大就行」,ALL 是「比每一个都大」。换成聚合函数就是一句话的事:
| 写法 | 等价于 | 记法 |
|---|---|---|
> ANY (...) |
> (SELECT MIN(...)) |
比小的那个还大 |
> ALL (...) |
> (SELECT MAX(...)) |
比大的那个还大 |
= ANY (...) |
IN (...) |
完全同一件事 |
<> ALL (...) |
NOT IN (...) |
完全同一件事 |
SOME 和 ANY 是同义词。 标准 SQL 里两个词都留着,
纯属历史遗留——写哪个都行,但别在同一份代码里混着用。
本沙箱不支持 ANY/ALL 关键字,演示里走的是等价的 MIN/MAX 写法,
两条结果完全一致。 这也顺带证明了一件事:这四条等价关系是真的,不是背下来的。
用的时候要留心 NULL。 > ANY 遇上全是 NULL 的子查询,结果也是未知;
> ALL 的逻辑更绕:<> ALL 才是「都不等于」,写错一个字意思全反。
五、三者在数据量上的差别
老文章里有一句流传很广的话:EXISTS 比 IN 快。 这句话有来历,但今天不能照搬。
| 写法(20 万行对 3 行小表) | 最快一次 | 相对 JOIN |
|---|---|---|
IN 子查询 |
43.49 ms | 慢 28% |
EXISTS 子查询 |
36.63 ms | 慢 8% |
直接 JOIN |
33.89 ms | 基准 |
实测里 IN 确实最慢,EXISTS 和 JOIN 基本并列。 但别急着下结论——
这三个数是在 SQLite 上测的,优化器的改写策略跟 SQL Server 不一样。
SQL Server 从 2005 起就会把 IN 改写成半连接,两者的执行计划常常一模一样。
当年「EXISTS 更快」的结论,是 SQL Server 2000 时代留下的。
所以写哪个应该看语义哪个清楚,不是看谁快。
如果要真的比性能,唯一靠谱的办法是在自己的库、自己的数据上跑执行计划看。
六、动手 Demo:NOT IN 的 NULL 陷阱与三者的实测差别
这一段先用六表样例库跑语义:IN 正常、NOT IN 正常,
然后往子查询里塞一个 NULL,看结果怎么被毒死。
接着验证 EXISTS 不读选择列表(用一个会抛错的函数证明),最后跑 ANY/ALL 的等价写法,
再在 20 万行大表上把 IN、EXISTS、JOIN 各测一遍。
import sqlite3, time
c = sqlite3.connect(u":memory:")
# ... 建六表、装样例数据,另插一个没成绩的学生 ...
# 1) NOT IN 正常
print(c.execute(u"SELECT sname FROM tb_student WHERE id NOT IN "
u"(SELECT student_id FROM tb_score)").fetchall())
# 2) 子查询混进 NULL
print(c.execute(u"SELECT sname FROM tb_student WHERE id NOT IN "
u"(SELECT student_id FROM tb_score UNION ALL SELECT NULL)").fetchall())
# 3) EXISTS 不读选择列表:塞个会抛错的函数进去
def boom():
raise sqlite3.OperationalError(u"boom 被求值了")
c.create_function(u"boom", 0, boom)
print(c.execute(u"SELECT COUNT(*) FROM tb_student st WHERE EXISTS "
u"(SELECT boom() FROM tb_score sc WHERE sc.student_id = st.id)"
).fetchone())
# 4) 20 万行大表上比 IN / EXISTS / JOIN
c.execute(u"CREATE TABLE big (id INTEGER PRIMARY KEY, sid INTEGER, v REAL)")
c.execute(u"WITH RECURSIVE t(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM t WHERE x < 200000) "
u"INSERT INTO big SELECT x, (x % 6) + 1, (x % 97) FROM t")
for tag, q in ((u"IN", q_in), (u"EXISTS", q_ex), (u"JOIN", q_jn)):
print(tag, u"%.2f ms" % clock(q))
实跑输出:
--------------------------------------------------------------------
IN、EXISTS、ANY 与 ALL 子查询
--------------------------------------------------------------------
① 使用 IN 的子查询
--------------------------------------------------------------------
1> SELECT sname
FROM tb_student
WHERE id IN (SELECT student_id FROM tb_score WHERE subject = '高等数学')
sname
-----
张伟
王芳
李娜
刘强
杨洋
(5 行受影响)
IN 的意思是「等于这一列里的任意一个」,等价于一串 OR。
NOT IN 有个坑,先看正常情况:
1> SELECT sname FROM tb_student WHERE id NOT IN (SELECT student_id FROM tb_score)
sname
-----
赵敏
(1 行受影响)
赵敏是刚插进来的第 7 个学生,一条成绩都没有,所以被 NOT IN 挑出来了。
现在往子查询里塞一个 NULL,看会发生什么:
1> SELECT sname
FROM tb_student
WHERE id NOT IN (SELECT student_id FROM tb_score UNION ALL SELECT NULL)
sname
-----
(0 行受影响)
! 0 行。不是数据没了,是 NOT IN 的语义被 NULL 毒死了。
NOT IN 展开成 id <> a AND id <> b AND id <> NULL。
任何值和 NULL 比,结果是「未知」;「未知」再参与 AND,整条就是未知。
WHERE 只留「真」,未知一律丢掉——所以整张表都过不去。
这就是为什么真实项目里 NOT IN 后面要跟 IS NOT NULL,或者干脆换 NOT EXISTS。
1> SELECT sname
FROM tb_student
WHERE id NOT IN (SELECT student_id FROM tb_score WHERE student_id IS NOT NULL)
sname
-----
赵敏
(1 行受影响)
② 使用 EXISTS 的子查询
--------------------------------------------------------------------
EXISTS 只问一句:这个子查询有没有行?有就是真,没有就是假。
它不看子查询 SELECT 后面写了什么,写 1、写 *、写常量都一样。
1> SELECT sname
FROM tb_student st
WHERE EXISTS (SELECT 1 FROM tb_score sc WHERE sc.student_id = st.id)
sname
-----
张伟
王芳
李娜
刘强
陈静
杨洋
(6 行受影响)
第 7 个学生赵敏没成绩,EXISTS 拿不到行,所以没出现在结果里。
证明「不看返回什么」:注册一个只会抛错的函数,塞进 EXISTS 的选择列表。
1> SELECT COUNT(*) AS 人数
FROM tb_student st
WHERE EXISTS (SELECT boom() FROM tb_score sc WHERE sc.student_id = st.id)
人数
----
6
(1 行受影响)
没有报错。boom() 写在 SELECT 后面,但 EXISTS 一次都没调用它。
它只关心「内层能不能捞出至少一行」,捞到就立刻返回真,剩下的列一律不算。
这也解释了为什么 EXISTS 里写 SELECT * 和写 SELECT 1 性能一样——都一样不读。
NOT EXISTS 是 NOT IN 的安全替代:
1> SELECT sname
FROM tb_student st
WHERE NOT EXISTS (SELECT 1 FROM tb_score sc WHERE sc.student_id = st.id)
sname
-----
赵敏
(1 行受影响)
结果和那条加了 IS NOT NULL 的 NOT IN 一样,但不需要你去操心 NULL。
③ 使用 ANY/SOME 的子查询
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT gname, price FROM tb_goods
| WHERE price > ANY (SELECT price FROM tb_goods WHERE type_id = 2);
! SQLite 没有 ANY/SOME 关键字,上面这条在这里会报语法错。
ANY 的意思是「比其中任意一个大就行」,等价于比最小值大。
1> SELECT gname, price
FROM tb_goods
WHERE price > (SELECT MIN(price) FROM tb_goods WHERE type_id = 2)
gname price
-----------------
联想笔记本 4999
戴尔显示器 1299
机械键盘 399
移动硬盘 549
打印机 899
(5 行受影响)
对比一下 = ANY 与 IN:
1> SELECT gname FROM tb_goods WHERE price = ANY (SELECT price FROM tb_goods WHERE type_id = 2)
消息 102,级别 15,状态 1
near "SELECT": syntax error
报错了。换成 IN 写法结果如下,两者在标准 SQL 里是同一件事。
1> SELECT gname, price
FROM tb_goods
WHERE price IN (SELECT price FROM tb_goods WHERE type_id = 2)
gname price
---------------
罗技鼠标 89.9
机械键盘 399
移动硬盘 549
(3 行受影响)
④ 使用 ALL 的子查询
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| SELECT gname, price FROM tb_goods
| WHERE price > ALL (SELECT price FROM tb_goods WHERE type_id = 2);
ALL 是「比每一个都大」,等价于比最大值还大。
1> SELECT gname, price
FROM tb_goods
WHERE price > (SELECT MAX(price) FROM tb_goods WHERE type_id = 2)
gname price
-----------------
联想笔记本 4999
戴尔显示器 1299
打印机 899
(3 行受影响)
| 写法 | 等价于 | 记法 |
|---|---|---|
| > ANY (...) | > (SELECT MIN(...)) | 比小的那个还大 |
| > ALL (...) | > (SELECT MAX(...)) | 比大的那个还大 |
| = ANY (...) | IN (...) | 完全同一件事 |
| <> ALL (...) | NOT IN (...) | 完全同一件事 |
ANY 和 SOME 是同义词,标准 SQL 里两个词都留着,纯属历史遗留。
⑤ IN、EXISTS、JOIN 三种写法在数据量上的差别:实测
--------------------------------------------------------------------
| 写法(20 万行大表对 3 行小表) | 最快一次(毫秒) |
|---|---:|
| IN 子查询 | 43.44 |
| EXISTS 子查询 | 35.24 |
| 直接 JOIN | 33.94 |
这三个数里 IN 最慢,比 JOIN 多花约 28%;EXISTS 和 JOIN 基本并列。
原因是 SQLite 把 IN 子查询按「建临时结果集再逐行比对」来跑,多一道工。
但不能拿这组数去说「EXISTS 一定比 IN 快」——那是 SQL Server 老版本流传下来的旧结论。
现代优化器普遍会把 IN 改写成半连接,两者执行计划常常一模一样。
写哪个应该看语义哪个清楚:能一句话说清「在里面」就用 IN,「存在」才用 EXISTS。
第 2 段那个「0 行」是本篇的核心。 它不报错、不警告,
就是安安静静地告诉你「没有符合条件的数据」——而实际上有。
这种错最难查,因为它看起来像个正常结果。 排查的时候可以先单独跑一下子查询,
看里面有没有 NULL;或者直接把 NOT IN 换成 NOT EXISTS 对比结果条数。
第 3 段那个 boom() 是真正的证据。 如果 EXISTS 会求值选择列表,这里一定抛异常;
它没抛,说明那句话从头到尾没被执行过。
最后那三个数(43.49 / 36.63 / 33.89)连在一起看才有意思。IN 多花了约一成到三成,
这个差距在 SQLite 上稳定存在,在 SQL Server 上未必——
所以别把它当成通用结论,要当成「又一次提醒:性能得自己测」。
30 秒实验
- 把
NOT IN换成NOT EXISTS,两个结果对比一次。 一条是 0 行,一条是 1 行——差的就是那个NULL。 - 把演示里
EXISTS的SELECT 1换成SELECT *,看计时变不变。不变。它压根不读选择列表。 - 写一条
> ANY和一条> (SELECT MIN(...)),把结果行数对比。 两条必须一样——如果不一样,说明你ANY那一条的条件写错了。
常见误区
- 「
NOT IN和NOT EXISTS只是写法不同」。碰上NULL天差地别。NOT IN静默返回空集,NOT EXISTS该给什么给什么。 子查询里可能有NULL的时候,一律用NOT EXISTS。 - 「
EXISTS里写SELECT *比写SELECT 1慢」。老的优化器可能有区别, 现代数据库里两者一模一样——因为选择列表整个被丢弃了。 - 「
EXISTS一定比IN快」。这是 SQL Server 2000 时代的经验。 现代优化器会改写,执行计划常常一样。 真要比较,得在自己的数据上看执行计划。
自测三问(点开对答案)
1. `NOT IN` 的子查询里有一个 `NULL`,结果会怎样? —— **返回空集,而且不报错。** 因为 `id <> NULL` 是「未知」,未知参与 `AND` 还是未知, **`WHERE` 只留真,未知全丢。** 2. `EXISTS` 会读 `SELECT` 后面的表达式吗? —— **不会。** 它只判断「能不能捞到至少一行」。**演示里塞了个会抛错的函数,一条错都没报。** **所以 `SELECT *` 和 `SELECT 1` 在 `EXISTS` 里性能一样。** 3. `> ANY` 和 `> ALL` 分别等价于什么? —— **`> ANY` 等价于 `> MIN`,`> ALL` 等价于 `> MAX`。** **记法:ANY 是「比小的那个还大」,ALL 是「比大的那个还大」。**下一篇 9.2 讲连接查询。子查询和连接在功能上有很多重叠,但连接是「一次把两张表摆在一起」, 效率通常更好——而且外连接是子查询做不到的。
- ☐ (本篇完,下一篇 9.2 连接查询)