EXISTS对外表逐行做布尔判断,只关心是否存在匹配;IN先物化子查询结果集再枚举比对,要求子查询单列且受NULL影响大。NOT IN遇NULL返回空结果,NOT EXISTS无此问题。
别被“都是子查询”骗了——EXISTS 是对外表逐行做「是否存在匹配」的布尔判断,而 IN 是先把子查询结果全捞出来,再做「值是否在集合中」的枚举比对。
这意味着:EXISTS 不关心子查询返回多少行,只要有一行就为真;IN 却必须把整个子结果集加载进内存(或临时

EXISTS 对外部表是 loop + 内部表索引查找(只用到内表索引)IN 实际走的是 hash join 或物化临时表,内外表都可能用上索引IN 要求子查询**只能返回一列**;EXISTS 无此限制(哪怕写 SELECT 1、SELECT * 都合法)关键看数据规模对比:不是看两张表谁大谁小,而是看「外表行数」vs「子查询结果集大小」。
EXISTS,避免把大结果集全拉出来SELECT id FROM status WHERE type = 'active' 只返回 5 条),外表却很大(百万级订单)→ 用 IN 更快,优化器容易走外表索引 + 内表等值查找NULL 值时,IN 会出逻辑陷阱(1 IN (1, NULL) 返回 NULL,不是 TRUE),而 EXISTS 完全不受影响SELECT * FROM orders o WHERE EXISTS ( SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 'active' );
这是线上最容易翻车的点:NOT IN 遇到子查询里任意一个 NULL,整条语句直接返回空结果集,不是你想要的「排除」,而是「全丢弃」。
NOT IN (1, 2, NULL) → 对任何非 NULL 值都判定为 UNKNOWN,WHERE 条件不成立NOT EXISTS 没这个问题,它只关心「有没有匹配行」,NULL 不干扰逻辑WHERE col IS NOT NULL,优化器也不一定能把这个过滤下推,风险仍在结论:只要涉及否定逻辑,无条件选 NOT EXISTS,别碰 NOT IN。
MySQL 8.0+ 的优化器已经很聪明,但它的选择仍高度依赖统计信息和实际数据分布。盲目替换可能适得其反。
EXPLAIN 看执行计划:关注 type(是否为 index/range)、rows(预估扫描行数)、Extra(有无 Using where; Using index)BENCHMARK() 或慢日志采样对比耗时GROUP BY + HAVING),IN 往往更稳定;EXISTS 在这类场景下可能退化成多次全表扫描真正可靠的策略不是记口诀,而是查 EXPLAIN、看 rows、测真实数据——尤其当子查询里有 JOIN 或函数时,两者的执行路径可能完全分叉。