SQL改写实战(续):子查询vs JOIN的深层原理

关键词​:子查询;JOIN;半连接;物化;查询优化器;SQL改写


大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

上周我们讲了COUNT进阶,这周回到SQL改写的一个经典话题:子查询和JOIN,到底哪个更快?网上有很多说法,"能用JOIN就别用子查询",但我在实际工作中见过反例------有时候子查询反而更快。今天我们从原理到实测,彻底讲透这件事。

一、子查询与JOIN的本质差异

  • JOIN:你把两个朋友圈(表A和表B)的所有人直接配对,找出共同认识的人。相当于先做笛卡尔积再过滤,但优化器会通过索引、哈希等算法优化。
  • 子查询:你先从表B中找出符合条件的人(子查询结果),然后拿着这个名单去表A中找对应的人。相当于分两步走。

从关系代数角度看,一个子查询可以改写为JOIN的前提是:子查询的结果集可以被"物化"为一个临时表,然后再与原表进行连接。但优化器是否会这样做,取决于代价估算。

二、优化器如何处理子查询

MySQL优化器对子查询的处理策略主要有以下几种:

策略 机制 适用场景 实例
半连接(semi-join) 将IN/EXISTS子查询转换为类似JOIN的操作,但只返回外表的行 子查询不复杂,结果集不大 SELECT * FROM t1 WHERE col IN (SELECT col FROM t2)
物化 先执行子查询,将结果存入临时表(带索引),再与外表连接 子查询结果集较小 同上半连接
EXISTS改写 对每一行外表,执行子查询判断是否存在 子查询结果集非常大,且外表小 SELECT * FROM t1 WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.id=t1.id)
相关子查询 子查询依赖外表的当前行,每行执行一次 无法改写为JOIN时 SELECT * FROM t1 WHERE col = (SELECT MAX(col) FROM t2 WHERE t2.id=t1.id)

重要 ​:MySQL从5.6版本开始引入了半连接优化,很多IN子查询会自动转为半连接,性能与JOIN相当。所以"子查询慢"的说法在MySQL 5.6+已经不成立了。

三、实测对比:子查询 vs JOIN

环境:MySQL 8.0.32,8C32G,表t1(1000万行),表t2(500万行),都有索引。

场景1:IN子查询(可转为半连接)

复制代码
-- 子查询
SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2 WHERE status = 1);
-- JOIN写法
SELECT t1.* FROM t1 JOIN t2 ON t1.id = t2.t1_id WHERE t2.status = 1;

实测结果:两者执行时间都在1.2-1.5秒之间,几乎无差别。查看执行计划,子查询被优化为半连接,实际执行相同的操作。

场景2:相关子查询(无法转为JOIN)

复制代码
-- 子查询(每行执行一次)
SELECT * FROM t1 WHERE amount > (SELECT AVG(amount) FROM t2 WHERE t2.cat_id = t1.cat_id);

这种相关子查询无法直接转为JOIN,改写JOIN需要先聚合再连接:

复制代码
SELECT t1.* FROM t1 
JOIN (SELECT cat_id, AVG(amount) as avg_amount FROM t2 GROUP BY cat_id) t2_avg 
ON t1.cat_id = t2_avg.cat_id AND t1.amount > t2_avg.avg_amount;

实测:子查询耗时38秒(每行执行一次子查询,1000万次),JOIN+聚合耗时0.9秒。此时JOIN完胜。

结论​:能否优化的关键不在于"子查询还是JOIN",而在于子查询是否可以被优化器转为半连接或物化。可转换的,性能相近;不可转换的(尤其是相关子查询),必须改写。

四、什么时候子查询可能比JOIN更快?

  • 子查询结果集极小:物化临时表比JOIN的哈希表构建更快。
  • 外表极大,子查询高度过滤:先执行子查询过滤掉大部分数据,再与外表连接,减少扫描量。
  • 子查询使用了覆盖索引:子查询直接从索引返回数据,避免回表。

示例:SELECT * FROM t1 WHERE id IN (SELECT id FROM t2 WHERE status = 1 LIMIT 10),子查询只返回10个id,物化成本极低,可能比JOIN全表扫描快。

五、系统化的改写决策

子查询类型 优化器处理 是否建议改写 改写方法
IN (SELECT ...) 子查询不依赖外表 半连接/物化 通常不需要 可保持原样
EXISTS (SELECT ...) 外表小、子查询大 外层循环 可改写为JOIN 改为JOININ
相关子查询(WHERE条件依赖外表) 每行执行一次 强烈建议 改为JOIN+聚合
NOT IN 子查询 可能产生全表扫描 建议 改为NOT EXISTSLEFT JOIN ... IS NULL
IN 子查询结果集极大 物化开销大 可考虑改写为EXISTS 改为EXISTS

特别注意 ​:NOT IN子查询在子查询结果包含NULL时,结果永远是空(因为NOT IN (NULL, 1, 2)的语义),这是SQL标准行为。建议改用NOT EXISTSLEFT JOIN,避免逻辑错误。

六、实战案例:优化一个慢查询

原SQL(耗时26秒):

复制代码
SELECT * FROM orders o 
WHERE o.status = 'PENDING' 
  AND o.order_date > '2026-01-01'
  AND o.user_id IN (SELECT user_id FROM users WHERE register_date < '2025-01-01');

执行计划显示:子查询被物化(用了索引),但物化表有200万行,然后外层表扫描后与物化表半连接,效率尚可,但仍有优化空间。

改写为JOIN(耗时18秒):

复制代码
SELECT o.* FROM orders o 
JOIN users u ON o.user_id = u.user_id
WHERE o.status = 'PENDING' 
  AND o.order_date > '2026-01-01'
  AND u.register_date < '2025-01-01';

为什么只快了一点?因为orders表本身有1000万行,索引过滤后仍有300万行,JOIN也需要扫描这些行。进一步优化:在orders(user_id, status, order_date)上建复合索引,并将查询改为覆盖索引,再回表取其他列,最终降到6秒。

启示​:不要只纠结子查询vs JOIN,索引设计往往影响更大。

七、总结

子查询和JOIN孰优孰劣,没有绝对答案,取决于优化器版本、数据分布、索引设计、子查询类型。掌握优化器处理子查询的机制(半连接、物化、相关),结合EXPLAIN分析执行计划,才能做出正确的改写决策。盲目相信"永远用JOIN"可能会错过子查询在某些场景下的优势。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

参考文献

  1. MySQL官方文档:《Subquery Optimization》
  2. 《高性能MySQL》第4版,第9章:查询优化
相关推荐
yyuuuzz2 小时前
独立站搭建的几个核心技术问题
运维·服务器·网络·数据库·aws
小蒋学算法2 小时前
redis分布式锁实现
数据库·redis·分布式
白菜欣2 小时前
【MySQL】MySQL数据的增删改查(入门版)
数据库·mysql
unicorn312 小时前
r-pan
数据库
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法
飞函安全3 小时前
飞函Webhook能力如何帮助企业把监控告警、设备异常第一时间推到对应群组
网络·数据库·安全·私有化im
map1e_zjc3 小时前
Redis入门笔记(2)
数据库·redis·笔记
开发者联盟league3 小时前
container登录失败解决方法。http: server gave HTTP response to HTTPS client
数据库·http·https
有想法的py工程师3 小时前
PostgreSQL分区表父索引INVALID排查实战:缺少某个分区索引导致父索引INVALID
数据库·postgresql