IN 和 EXISTS 的区别与优化

IN 和 EXISTS 的区别与优化

两个常见的子查询方式

-- IN 子查询
SELECT * FROM user
WHERE id IN (SELECT user_id FROM order WHERE amount > 1000);

-- EXISTS 子查询
SELECT * FROM user u
WHERE EXISTS (SELECT 1 FROM order o 
              WHERE o.user_id = u.id AND o.amount > 1000);

两者对外部查询来说语义相同:返回所有存在大额订单的用户。但在执行机制和性能上存在本质区别。

执行机制区别

IN 的执行流程

-- MySQL 处理 IN 子查询的逻辑:
-- 1. 先执行子查询:SELECT user_id FROM order WHERE amount > 1000
--    结果集:[1, 3, 5, 7, 9]
-- 2. 将结果集作为 IN 列表:WHERE id IN (1, 3, 5, 7, 9)
-- 3. 对外部表执行等值匹配

旧版本(MySQL 5.5 以下)的 IN 性能问题
子查询结果集很大的情况下,IN 列表会非常长,对优化器不友好。

MySQL 5.6+ 的优化
优化器会将 IN 子查询转换为 Semi-Join(半连接):

-- 优化器将 IN 子查询等效转换为
SELECT u.* FROM user u
SEMI JOIN order o ON u.id = o.user_id
WHERE o.amount > 1000;

Semi-Join 的优化在于:一旦找到匹配行就停止扫描,不会继续匹配其他行。

EXISTS 的执行流程

-- MySQL 处理 EXISTS 子查询的逻辑:
-- 1. 取外部表 user 的一行
-- 2. 对每一行执行子查询:EXISTS (SELECT 1 FROM order WHERE user_id = u.id AND amount > 1000)
-- 3. 如果子查询有结果,该行加入结果集
-- 4. 重复 1-3 直到外部表遍历完毕

EXISTS 是”外部驱动内部”的方式:外部表 user 有多少行,子查询就执行多少次。

性能对比的关键因素

因素一:驱动表的大小

SELECT * FROM user WHERE id IN (SELECT user_id FROM order WHERE amount > 1000)

假设:
- user 表:100 万行(大表)
- order 表:10 万行满足 amount > 1000(小结果集)

IN 的流程:子查询执行 1 次 → 结果集 10 万 → 与 user 表匹配
          (优:子查询结果集小)

EXISTS 的流程:外部表 user 100 万行 → 执行 100 万次子查询
              (劣:子查询执行次数过多)
-- 大表驱动的 EXISTS 很慢
SELECT * FROM user u   -- user 100 万行
WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id);
-- 子查询会执行 100 万次!

因素二:子查询相关索引

-- 有索引时 EXISTS 效率提升
SELECT * FROM user u
WHERE EXISTS (
    SELECT 1 FROM order o 
    WHERE o.user_id = u.id  -- 如果 order.user_id 有索引,每次索引查找很快
);
-- 索引情况下,100 万次索引查找 ≈ 100 万 × 3-4 次 IO

-- 无索引时 EXISTS 非常慢
-- 100 万次 × 全表扫描 100 万行 = 不可接受的代价

通用经验法则

条件 IN 更快 EXISTS 更快
外部表小,子查询结果集大
外部表大,子查询结果集小
外部表 + 子查询表都大,索引存在 相近 相近
子查询包含 OR 条件
子查询需要关联外部表字段 ✅(唯一选择)
子查询结果集为空 ✅(直接返回空) ❌(仍需遍历外部表)

综上

  • IN 适合:子查询结果集较小,且该结果集需要被多次引用
  • EXISTS 适合:子查询表有索引,且外部表较小
  • 大多数场景下,MySQL 5.6+ 的优化器会将 IN 转换为 Semi-Join,性能已经很接近

特殊场景分析

场景:NOT IN vs NOT EXISTS

-- NOT IN:需要子查询结果集无 NULL
SELECT * FROM user WHERE id NOT IN (SELECT user_id FROM order);
-- 如果子查询结果集中有 NULL,NOT IN 会返回空结果
-- 因为:NULL 参与 NOT IN 比较时,结果总是 UNKNOWN

-- NOT EXISTS:没有此问题
SELECT * FROM user u
WHERE NOT EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id);
-- 不需要处理 NULL 的特殊情况

结论:NOT EXISTS 通常比 NOT IN 更安全,且性能更好。

场景:IN + LIMIT

-- 返回 10 个大额订单的用户
SELECT * FROM user
WHERE id IN (SELECT user_id FROM order WHERE amount > 1000)
LIMIT 10;

-- IN 子查询会先计算全部结果集(可能很大),再 LIMIT
-- 换成 EXISTS + LIMIT 可能更优(外部遍历找到 10 条就停)

用 JOIN 替代的时机

IN 和 EXISTS 本质上都可以用 JOIN 改写:

-- IN 写法
SELECT * FROM user WHERE id IN (SELECT user_id FROM order);

-- JOIN 改写(需要去重)
SELECT DISTINCT u.* FROM user u JOIN order o ON u.id = o.user_id;

-- EXISTS 写法
SELECT * FROM user u
WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.id);

-- 三者语义等价,但优化器处理不同
-- MySQL 5.6+ 对三者的 SQL 执行计划可能完全相同

面试要点

  • IN 和 EXISTS 的核心区别是”执行方向”不同:IN 子查询 → 外部表,EXISTS 外部表 → 子查询
  • 小表驱动大表原则适用于两者的选择
  • 索引对 EXISTS 至关重要(否则子查询执行多次就成了灾难)
  • NOT EXISTS 比 NOT IN 更安全(NULL 处理问题)
  • MySQL 5.6+ 的 Semi-Join 优化使得两者性能差距缩小
© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容