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章:查询优化
相关推荐
Csvn8 小时前
📊 SQL 入门 Day 7:子查询 — 查询中的查询
后端·sql
2401_8414956410 小时前
【数据结构】B+树
数据结构·数据库·c++·b+树·概念·结构·操作原理
Hardworking66611 小时前
第7章 软硬件系统集成
数据库·软硬件系统集成
名字还没想好☜12 小时前
Go 的 time.Ticker 陷阱:定时任务里被忽略的内存泄漏与正确关闭
java·数据库·golang·go·定时器
熊猫钓鱼>_>12 小时前
Redis 突发缓存穿透:一次完整的定位复盘
数据库·人工智能·redis·缓存·ai·agent·智能
wenb1n13 小时前
MySQL诊断系列(3/6):索引分析——5个SQL揪出“僵尸索引”
数据库·人工智能·编程语言
段一凡-华北理工大学13 小时前
向量数据库实战:选型、调优与落地~系列文章03:向量相似度算法全解:余弦、欧氏、内积,到底该用哪个?
大数据·数据库·人工智能·算法·机器学习·向量相似度·高炉炼铁
ClouGence14 小时前
CloudDM 数据库管理平台,全新 UI,更清晰、更高效!
数据库·开源
花生了什么事o16 小时前
DDD:领域驱动设计的初步认识
java·数据库
BGK11235816 小时前
基于qemu_v8+optee 4.00 平台构建 ca/ta
java·大数据·数据库