IN先物化子查询再比对,EXISTS逐行执行支持短路;IN遇NULL返回UNKNOWN导致漏查,NOT IN遇NULL恒为空集,NOT EXISTS无此问题;性能取决于索引与执行计划。

IN 先物化子查询,再逐行比对
IN 的执行是「两阶段」:先完整跑完子查询,把结果全部加载进内存(临时哈希表),再扫描主表每一行,用 WHERE 字段去查这个哈希表。这意味着子查询哪怕只差一行就匹配上,也必须等它完全执行完毕。
- 子查询执行 1 次,但结果集必须全量返回、去重、建索引(哈希)
- 主表扫描时,若
WHERE字段无索引,就会触发type: ALL,性能断崖下跌 - 子查询里只要出现一个
NULL,整个IN表达式变成UNKNOWN,该行直接被过滤掉(不是慢,是逻辑错误) - 示例:
SELECT * FROM orders WHERE customer_id IN (SELECT id FROM customers)——customers.id若含NULL,所有订单都查不到
EXISTS 是“每行一次子查询”,支持短路
EXISTS 不关心子查询返回什么值,只看是否至少有一行。它对外表(主查询)逐行处理,每行代入子查询条件中执行一次,一旦命中就立刻停,不继续查剩余行。
- 子查询执行次数 = 主表扫描行数(比如主表 10 万行,子查询最多执行 10 万次)
- 能否快,取决于子查询是否能走索引 —— 关键是内表(子查询里的表)的关联字段要有索引,例如
orders(user_id) -
EXISTS对NULL完全免疫:子查询里字段为NULL或结果含NULL,只要返回一行,就算TRUE - 示例:
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)—— 即使orders.user_id有NULL,也不影响判断
看执行计划比背口诀更靠谱
别靠“小表驱动大表”这种模糊经验做选择。同一语义的 SQL,EXPLAIN 出来的 rows、type、key 才是真实依据。
-
IN计划里出现materialized+Using where; Using index(外表字段走索引),说明它正在高效哈希匹配 -
EXISTS计划里看到dependent subquery或semijoin,且子查询部分有Using index(内表字段走索引),才真正发挥优势 - 如果
EXISTS子查询里没索引,它会退化成嵌套循环全扫,比IN还慢 - 用
EXPLAIN FORMAT=TREE(MySQL 8.0+)能更清楚看到物化 vs 循环结构
NOT IN 的陷阱比性能问题更致命
只要子查询结果里有一个 NULL,NOT IN 就永远返回空集 —— 这是 SQL 三值逻辑的硬伤,不是优化能解决的。
-
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM logs):哪怕logs.user_id只有一条NULL,整条语句查不出任何用户 -
NOT EXISTS没这个问题,它只判断“有没有匹配行”,NULL不参与布尔计算 - 即使你确认当前数据无
NULL,也建议统一用NOT EXISTS—— 避免后续加字段、改约束时突然崩掉
真正卡住人的,从来不是“哪个更快”,而是 NULL 导致的逻辑错、索引没生效时的隐性全表扫、以及执行计划里藏得极深的 materialized 内存膨胀。写完记得 EXPLAIN 一眼,比猜强十倍。


















