一直没想明白一个问题:同样是查一组数据,为什么有人写JOIN秒回,有人写JOIN直接拖垮整个接口;同样是查不在某个集合里的记录,为什么IN和NOT IN的表现天差地别;还有一个需求,分了两段SQL查出来都很快,合并成UNION后却等了好几秒。如果你经历过这些,那这篇MySQL查询优化的内容应该正好对你胃口。
无论你是刚接触MySQL的开发新人,还是维护过千万级数据表的后端工程师,JOIN、子查询、UNION这三个关键字都会反复出现在你的SQL里。它们并不高深,但被误用的概率极高。很多人习惯性拿到需求就写多表JOIN,遇到集合判断就套子查询,需要合并结果就上UNION,却没想过这三种写法的底层执行逻辑完全不同,性能差异可以到达几个数量级。这篇文章会把三者的区别、适用场景、优化器行为、常见坑位一次讲透,并配合实际可复现的SQL用例和EXPLAIN解读,帮助你在写SQL时先对执行成本有个大致预判,而不是等线上慢查询出来了再去救火。
1. 三种写法的本质差异:先把执行逻辑在脑中跑一遍
在谈性能之前,我建议你先在心里问一个问题:我到底想让数据库做什么?很多SQL写出来慢,不是数据库不行,而是开发者根本没想清楚自己要的是"行的扩展"还是"列的扩展",是"先得到集合A再判断和集合B的关系",还是"把两个结果堆在一起"。
JOIN的核心动作是配对,它做的事情是“横向扩展”。假设有两张表,一张是用户表,一张是订单表,通过用户ID配对后,输出结果里的列是"用户表的列 + 订单表的列",行的数量取决于匹配关系,一对一返回一行,一对多就会复制出多行。这就像你把两本电话簿用同一个手机号拼在一起,如果一个人名下有三部电话,就会在结果里出现三行。
子查询的核心动作是分步,先算出内部的结果集,再把这个结果交给外层去用。最典型的三类:标量子查询返回一个值,列子查询返回一列值,表子查询返回一张临时表。它相当于先拿一张小纸条,把部门ID列表抄下来,再去主表里挨个对照。由于MySQL优化器对子查询的处理策略特别多,同一个子查询在不同版本、不同索引条件下表现可能迥异,这也是后面要重点展开的地方。
UNION的核心动作是纵向堆叠,把所有分支的结果集按行上下拼接,唯一的硬性要求是每个分支的列数相同、对应列的类型兼容。判断用JOIN还是UNION其实有个笨办法:你想让结果变宽,找JOIN;你想让结果变长,找UNION。如果你最初只想要两段互相独立的数据列表放在同一份报表里,UNION显然是语义最贴切的选择,这时候硬要用JOIN反而会把问题复杂化。
这三种写法的性能差异,本质上来自它们各自引发的执行动作不同。JOIN会关心驱动表和被驱动表的连接方式;子查询会关心是否被优化为半连接、物化表还是逐行执行;UNION会关心是否产生去重临时表。把它们先看清,才知道后面怎么调。
| 对比项 | JOIN | 子查询 | UNION |
|---|---|---|---|
| 扩展方向 | 横向加列 | 纵向过滤或提供集合 | 纵向加行 |
| 核心动作 | 行配对 | 先算范围再判断 | 结果集堆叠 |
| 典型应用 | 多表字段合并 | where/from条件筛选 | 分表、分条件数据聚合 |
| 常见痛点 | 一对多行数膨胀 | 相关子查询逐行执行 | UNION默认去重开销 |
| 最常出现的执行计划信息 | Nested Loop、Hash Join | Dependent subquery、Materialize | Using temporary、Using filesort |
1.1 JOIN是对行的配对,理解一对多放大是第一步
很多慢SQL都是因为JOIN到了一张多明细的表上。比如订单主表和订单明细表关联,一个订单有5条明细,JOIN后原来一行订单会被撑成5行。如果后面还有聚合计算,这会直接放大聚合代价。所以在写JOIN的时候,第一步该确认的不是用什么连接类型,而是连接双方在关联键上是否唯一。连接键有唯一性约束的那边,叫做"维度侧";没有唯一性、会产生多行匹配的那边,叫做"事实侧"。最理想的情况是主键或业务编号驱动小表,去连接唯一键。
1.2 子查询是"先定范围再行动",但要小心它变成逐行执行
子查询在语义上是"分步"的,但优化器不一定会真的先物化一个集合。MySQL会尝试把很多子查询改写成JOIN或半连接,改不了的时候就可能变成相关子查询,也就是外层每一行都触发一次内层查询。这也是"子查询慢"最大的来源。后面我会用EXPLAIN带你识别它到底走了哪条路。
1.3 UNION是结果集叠加,别让它偷偷多做一件去重的事
UNION和UNION ALL最大的差异就一个字:去重。UNION默认会去掉重复行,UNION ALL不会。别小看去重,为了实现它,MySQL可能要把结果放进临时表,给结果列建唯一索引,甚至走文件排序,数据量一大就会非常难受。当你能用业务逻辑保证两个分支的结果没有交集时,直接上UNION ALL是更务实的选择。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JOIN性能调优:驱动表、索引与"为什么大厂不建议多用多表JOIN"的工程原因
回到JOIN本身的性能问题。MySQL执行JOIN,最常见的方式还是嵌套循环,也就是从驱动表取一行,去被驱动表里找匹配行,循环往复。等值连接且关联列没索引时,MySQL 8.0以后会退化成Hash Join,这比逐行扫描强一些,但本质上还是因为没有可用的连接索引。如果你在EXPLAIN里看到了Hash Join,先别高兴,第一反应应该是:右边那张表的关联列是不是漏建索引了?
这个执行模型决定了一条最基本的优化铁律:驱动表要小,被驱动表的连接列必须有索引。原因很直白,驱动表每多一行,都要去被驱动表做一次索引查找,所以驱动表的行数对查询总成本是近似倍数关系的影响。MySQL优化器虽然会根据表统计信息自动选择驱动表,但统计信息不一定准,或者因为表数据分布变化,它也可能选错。遇到这种情况,可以先尝试ANALYZE TABLE刷新统计信息,如果仍然选错,再用STRAIGHT_JOIN强制指定连接顺序来验证。
2.1 不要被表面语义带偏:LEFT JOIN和RIGHT JOIN该怎么选
需要关联表时,很多人随口就写LEFT JOIN,但LEFT JOIN和RIGHT JOIN本质是同一个东西的镜像。LEFT JOIN是以左表为保留表,右表没匹配上就补NULL;RIGHT JOIN则是反过来。既然可以靠调换表的顺序表达同样语义,绝大多实践里统一用LEFT JOIN就够了,RIGHT JOIN显得多余且容易让阅读者犯迷糊。
真正要注意的坑是:LEFT JO
