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


暂无评论内容