没有固定优劣,需依查询结构和数据分布选择:in适合子查询小、外表大且有索引;exists适合外表小、内表大且关联字段有索引;not in遇null会返回空结果,应优先用not exists;务必通过explain验证执行计划。

没有“哪个更优”的固定答案,只有“哪个更匹配当前查询结构和数据分布”的选择。盲目替换 IN 和 EXISTS 可能让查询变慢十倍以上。
子查询结果小、外表大且有索引时,IN 往往更快
比如查“所有属于数字类目的商品”,categories 表只有几十行,products.category_id 有索引:
SELECT * FROM products WHERE category_id IN (SELECT id FROM categories WHERE type = 'digital');
MySQL 5.6+ 会把子查询物化成哈希表,再用 products 的索引快速定位——外表扫描少、内存开销可控。
- 子查询必须返回单列,多列或空结果会报错或逻辑异常
- 若子查询结果超 1 万行,
IN容易触发Using temporary,内存暴涨,EXPLAIN中type可能退化为ALL -
IN (1, 2, 3)这种字面量写法永远最快,不走子查询执行流程
外表小、内表大且关联字段有索引时,EXISTS 更稳
比如查“所有下过付费订单的用户”,users 表几万行,orders 表百万级,orders.user_id 有索引:
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid');
优化器会转为半连接,每查一个 u.id,就用索引在 orders 里找一条匹配即停——不建临时表,不加载全量数据。
- 子查询里必须包含对外表字段的引用(如
o.user_id = u.id),否则变成“只要 orders 里有任意一条 paid 订单,就返回所有 users” -
SELECT 1是惯用写法,比SELECT *或SELECT id更轻量,MySQL 不解析列内容 - 如果
users表本身没过滤条件、又没索引,EXISTS的逐行调用反而放大扫描代价
NOT IN 是隐形陷阱,NOT EXISTS 几乎总是更好
NOT IN 遇到子查询结果含 NULL,整个条件恒为 UNKNOWN,结果集直接为空——这不是慢,是错。
例如:SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM logs),只要 logs.user_id 允许为 NULL,哪怕只有一条,就查不出任何用户。
-
NOT EXISTS不受NULL影响,语义正确,还能走内表索引 - 即使你确认当前无
NULL,也建议统一用NOT EXISTS,避免未来加字段或改约束时埋雷 - 性能上,
NOT EXISTS基本总是快于NOT IN,因为后者常被迫全表扫描内外表
别信经验,必须看 EXPLAIN
MySQL 5.7 和 8.0 对同一语句可能生成完全不同执行计划;统计信息不准、索引缺失、optimizer_switch 设置都会影响选择。
重点对比 EXPLAIN FORMAT=TRADITIONAL 输出中的三列:
-
type:是否用了索引(ref/range好,ALL危险) -
rows:预估扫描行数(越小越好) -
key:实际命中哪个索引(NULL表示没用上)
真正决定效率的是「外表扫描行数」和「子查询结果集大小」,不是物理表大小。很多翻车案例,都是只换关键字,却漏掉子查询结构变化带来的隐性代价——比如把 IN 改成 EXISTS 后,子查询里没补上 WHERE 关联条件。











