子查询 vs JOIN 查询性能比较:谁更快?什么时候用?
在写 SQL 的时候,经常会纠结:
“这里要用子查询好,还是 JOIN 好?”
“听说子查询性能差,是真的吗?”
“JOIN 会不会比子查询更重?”
这篇文章一次把两者的原理与性能差异讲清楚。
一、什么是子查询与 JOIN?
✅ 子查询(Subquery)
将一个查询嵌入到另一个查询中:
SELECT *
FROM user
WHERE id IN (SELECT user_id FROM orders);
✅ JOIN 查询(连接查询)
SELECT u.*, o.*
FROM user u
JOIN orders o ON u.id = o.user_id;
两者都能实现“关联多表”的目的,但执行原理不同。
二、性能核心差异总结
JOIN 一般比子查询更快,中大型数据量下几乎总是推荐 JOIN。
原因:JOIN 可以使用优化器对多表进行统一优化,而很多子查询(尤其是相关子查询)不能被完全优化。
三、为什么 JOIN 通常比子查询快?
1)JOIN 可以被 MySQL 优化器重写、索引优化更充分
JOIN 是显式的关联方式:
-
• 可以使用嵌套循环(Nested Loop) -
• 可以使用驱动表/被驱动表策略 -
• 可以使用索引 join -
• 可以选择更优的执行顺序 -
• 可以提前过滤数据(WHERE pushdown)
子查询(尤其是相关子查询)无法做到这些优化。
2)JOIN 只扫描必要的表,而子查询容易“重复扫描”
例如相关子查询:
SELECT name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_cnt
FROM user u;
如果 users 有 1 万条记录:
-
• 子查询执行 10000 次 -
• 每次扫描 orders(很可能没有被缓存)
成本巨大。
JOIN 查询效果要好得多:
SELECT u.name, COUNT(o.id)
FROM user u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
JOIN 只扫描 一次 orders 表。
3)子查询可能导致临时表(temp table),JOIN 不一定
例如:
SELECT * FROM orders
WHERE user_id IN (SELECT id FROM user WHERE status = 1);
MySQL 会把子查询结果写入临时表,再扫描判断 IN,可能是磁盘临时表:
-
• 额外 I/O -
• 内存/磁盘开销大
JOIN 则不需要额外的临时表结构,优化器可以直接利用索引进行过滤:
SELECT o.*
FROM orders o
JOIN user u ON o.user_id = u.id
WHERE u.status = 1;
四、是否子查询一定性能差?并不是!
MySQL 8.0 之后改进很大,某些子查询已经可以被优化器“合并重写”为 JOIN。
例如:
SELECT *
FROM orders
WHERE user_id IN (SELECT id FROM user);
MySQL 可能自动重写成 JOIN。
但以下场景仍然不能优化:
❌ 相关子查询
❌ 使用聚合函数的子查询
❌ 子查询依赖外层列
❌ 子查询中包含 LIMIT、ORDER BY
❌ 大子查询 + IN/NOT IN
❌ ANY/ALL/SOME 子查询
这些情况下 JOIN 显著优于子查询。
五、什么时候推荐 JOIN?
✔ 数据量大
✔ 复杂业务逻辑
✔ 需要多个关联字段过滤
✔ 子查询结果非常大
✔ 性能要求高的业务(订单流、推荐流、榜单、日志系统)
大部分实际业务场景都是这一类。
六、什么时候推荐子查询?(少数场景)
✔ 子查询结果非常小(例如 1 条)
✔ 子查询和主查询没有关联(非相关子查询)
✔ 比 JOIN 写法更直观(可读性优先)
✔ 想减少返回列数量(只希望过滤,不希望 JOIN 导致行增多)
✔ 简单 IN 子查询(MySQL 8.0 已可自动优化)
比如统计类 SQL:
SELECT *
FROM orders
WHERE amount > (SELECT AVG(amount) FROM orders);
JOIN 无法替代。
七、总结
1、子查询 vs JOIN 的性能差异主要来自执行方式不同。
2、 JOIN 使用的是嵌套循环,可使用索引、驱动表优化、提前过滤等优势,一般性能更好。
3、而子查询可能导致多次扫描、临时表、大量重复执行,尤其是相关子查询,性能最差。
4、 MySQL 8.0 对部分子查询进行了优化,可以自动转为 JOIN,但不是全部。
实际开发中:
多表关联优先使用 JOIN;相关子查询尽量改写为 JOIN;子查询只在结果小且不适合 JOIN 时使用。

