执行计划中的type和extra字段才是判断in与exists性能的关键:type=dependent subquery表示嵌套循环,性能差;type=eq_ref或ref加using index说明走半连接,性能接近;含外层字段的相关子查询使in无法优化,而exists天然适配;null会导致in逻辑错误,exists则不受影响。

别猜,直接看执行计划——EXPLAIN 输出里的 type 和 Extra 字段才是真实答案。IN 和 EXISTS 谁快,不取决于语法本身,而取决于优化器实际走了哪条路。
查执行计划时重点盯这两个字段
MySQL 用 EXPLAIN FORMAT=TREE(8.0+)或传统 EXPLAIN 就能判断:
-
type=DEPENDENT SUBQUERY:说明是“嵌套循环”,没走优化,IN和EXISTS都会慢,尤其外层表大时 -
type=eq_ref或type=ref+Extra=Using index:说明用了索引,大概率走的是半连接(semi-join),这时两者性能接近 -
Extra=Using where; Using index for group-by或出现Semi-join关键字:确认优化器已将IN自动转成半连接,不用硬切EXISTS -
Extra=Select tables optimized away:子查询结果极小(比如单值或空),IN可能更快,甚至被常量折叠
子查询是否含外层字段,决定优化器能不能发力
只要子查询里引用了外层表字段(即“相关子查询”),IN 基本就失去被改写为半连接的机会,只能老老实实嵌套执行;而 EXISTS 天然就是相关子查询,反而更容易被索引驱动。
- 能被优化的
IN:例如WHERE id IN (SELECT user_id FROM orders WHERE status = 'paid')(无外层列引用) - 大概率无法优化的
IN:例如WHERE id IN (SELECT user_id FROM orders WHERE order_time > u.created_at)(用了外层表u的字段) -
EXISTS写法天然适配相关逻辑:WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid'),且只要orders.user_id有索引,就能快速定位
NULL 值不是性能问题,但会让结果“静默消失”
这是比性能更危险的问题:IN 遇到子查询返回任意 NULL(哪怕只有一行),整个条件判为 UNKNOWN,对应主表行直接被过滤掉,不报错、不提示、查不到数据——你可能以为是慢,其实是逻辑错了。
-
NOT IN更致命:只要子查询里有NULL,整条语句查不到任何结果 -
EXISTS和NOT EXISTS完全无视NULL,只看是否存在满足WHERE条件的行,行为稳定可预期 - 如果必须用
IN且子查询字段可能为NULL,得显式加过滤:WHERE id IN (SELECT user_id FROM orders WHERE user_id IS NOT NULL)
真正影响快慢的,从来不是 IN 还是 EXISTS 这个选择,而是子查询字段有没有索引、外层表大小、以及优化器是否成功把查询转成了半连接。写完语句后第一件事不是改写语法,是跑一遍 EXPLAIN,看它到底干了什么。











