直接回答:IN 是把子查询结果集先算出来再做集合匹配;EXISTS 是拿外表的每一行去子查询里做存在性探测,查到一行就短路返回。现代 MySQL(5.6+)优化器会把相关子查询改写成 semi-join(materialization、first-match 等策略),两者常被优化成相同的执行计划,性能差异远没有传闻中大,应以 EXPLAIN 为准,而不是背口诀。

展开解析:传统经验是“小表驱动大表”:子查询结果集小用 IN,外表小而内表大且有索引用 EXISTS。IN 后面跟常量列表(如 status IN (1,2,3))会走 range 索引扫描,这与子查询无关,放心用。EXISTS 查到第一行即短路返回,不需要物化完整结果集,列上写 SELECT 1 还是 SELECT * 对性能没有影响。真正致命的区别在 NULL:NOT IN 的子查询结果只要含一个 NULL,每一行的比较结果都变成 UNKNOWN,整个查询返回空集,这是生产事故高发点;NOT EXISTS 不受 NULL 影响,所以做“反连接”一律用 NOT EXISTS 或 LEFT JOIN ... IS NULL。JOIN 用于需要取两边列的场景,但一对多 JOIN 会放大行数,仅做过滤时不要用它代替 IN/EXISTS,更不能靠 SELECT DISTINCT 去重来掩盖笛卡尔放大——那是在掩盖建模或写法问题。

-- 危险:banned_id 含 NULL 时返回空集
SELECT * FROM orders WHERE user_id NOT IN (SELECT banned_id FROM blacklist);
-- 安全写法
SELECT * FROM orders o WHERE NOT EXISTS
  (SELECT 1 FROM blacklist b WHERE b.banned_id = o.user_id);

追问方向:semi-join 有哪些执行策略?为什么 NOT IN 遇 NULL 返回空集?IN 跟常量列表时如何走索引? (约 354 字)