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 改为JOIN或IN
相关子查询(WHERE条件依赖外表) 每行执行一次 强烈建议 改为JOIN+聚合
NOT IN 子查询 可能产生全表扫描 建议 改为NOT EXISTS或LEFT JOIN ... IS NULL
IN 子查询结果集极大 物化开销大 可考虑改写为EXISTS 改为EXISTS

​特别注意 ​:NOT IN子查询在子查询结果包含NULL时,结果永远是空(因为NOT IN (NULL, 1, 2)的语义),这是SQL标准行为。建议改用NOT EXISTS或LEFT 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章:查询优化
相关推荐
摇滚侠2 小时前
《On Java 中文版 基础卷》阅读笔记 实现隐藏 12
java·数据库·笔记
川石课堂软件测试2 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
2501_937767163 小时前
自建 SQL 数据库定时备份踩坑复盘与落地思路
数据库·sql
silvamux_4 小时前
10款模型写种草笔记:可发布性、返工来源与成本数据缺口复盘
java·数据库·笔记
I Am a robert girl6 小时前
从 GameHorizon 拆解多时间尺度游戏评测:数据管线、离线基准与在线回放的三层设计
数据库·游戏·多时间尺度·数据管线·游戏评测·离线基准·在线回放
躺柒6 小时前
读数据安全架构设计与实践17安全架构案例(下)
数据库·安全·零信任·安全架构·防火墙·无边界
SelectDB7 小时前
实时更新排错现场:导入了却查到旧数据,和 FINAL 越加越慢的两条路
大数据·数据库·数据分析
SelectDB7 小时前
拿 Planet 的账单一行行对下来,我总结出三个可自查的校验点
大数据·数据库·数据分析
SelectDB7 小时前
Apache Doris + Vane:让多模态数据进入业务分析
大数据·数据库·数据分析
pjj198549 小时前
ClickHouse 与 Doris 数据库技术梳理:看懂分析型数据库
数据库·clickhouse