做 MySQL 开发这些年,我见过太多因为查询写法不当引发的线上事故。JOIN、子查询、UNION 这三个 SQL 武器,几乎每天都在和后端代码打交道。用得好,能把几百毫秒的慢查询压到几毫秒;用不好,一条看似简单的语句就能把数据库 CPU 打满。这篇文章就结合我实际踩过的坑,把三者的选型逻辑和调优手段完整讲透。适合后端开发、DBA、正在准备 MySQL 面试的同学参考。
1. 为什么需要三剑客:从一条慢查询说起
1.1 一条线上事故的复盘
记得有一次,业务方反馈某个报表接口超时,数据库 CPU 直接飙到 90% 以上。我拉出慢查询日志,发现罪魁祸首是下面这条语句:
sql复制SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id
JOIN order_items oi ON o.id = oi.order_id
JOIN payments pay ON o.id = pay.order_id
JOIN ...
这条 SQL 一口气 JOIN 了七张表。虽然每张表的记录量都不算夸张,但 JOIN 的笛卡尔积放大效应导致扫描行数呈指数级增长。后来我把它拆成两条查询,用临时表承接中间结果,响应时间从 3 秒降到 80 毫秒。这次事件让我真正意识到,JOIN 不是不能用,而是得有策略地用。
1.2 三剑客各解决什么问题
JOIN、子查询、UNION 在 SQL 里干的活不一样:
- JOIN:把多张表的行按关联条件横向拼接到一起,适合读取"一个主对象以及它关联的明细/维度信息"。
- 子查询:在一条语句中内嵌查询结果,适合"先算一批中间结果,再基于它做二次筛选"的场景。
- UNION:把多个 SELECT 的结果集纵向合并,适合"同一结构的数据来自不同表,需要统一返回"。
如果你能清晰地说出上面这句话,选型时基本就不会跑偏。接下来我分别拆细节。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JOIN:多表关联的正确打开方式
2.1 JOIN 类型与执行逻辑
JOIN 大致分 INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL JOIN(MySQL 原生不支持,需要模拟)。它们的关键区别是保留哪一侧的行:
- INNER JOIN:只保留两表匹配上的行。
- LEFT JOIN:左表全保留,右表无匹配补 NULL。
- RIGHT JOIN:右表全保留,左表无匹配补 NULL。
- FULL JOIN:两表全保留,MySQL 里要用 LEFT JOIN + UNION 来实现。
执行逻辑上,MySQL 8.0 之前的优化器主要用 Nested Loop Join(嵌套循环连接),8.0.20 之后默认支持 Hash Join。简单理解:
- Nested Loop:从驱动表拿一行,去匹配另一张表。
- Hash Join:把一张表建成哈希表,再遍历另一张表去探测。
用生活类比:Nested Loop 像你拿本通讯录,挨个打电话问对方有没有你要的型号;Hash Join 是先在 Excel 里做好索引,再快速查。
选择驱动表时,优化器倾向用小表作为驱动表,因为外层循环次数越少,总匹配次数越少。这里有个容易忽略的点:LEFT JOIN 中 LEFT 侧表不一定永远作为驱动表,优化器仍然会基于估算成本决定执行顺序,这是很多人理解上的盲区。
2.2 为什么大厂不建议多表 JOIN
很多面试题都会问"为什么大厂不建议使用多表 join",我的理解是:
- 可维护性差:一张 SQL 里 join 五六张表,代码评审的人看你都像是在看天书。后面接手的工程师想改需求,第一反应是重写。
- 性能不可控:join 越多,执行计划越复杂,优化器选错索引的概率越高。数据量一旦增长,扫描放大效应迅速把数据库拖垮。
- 扩展性差:当单表数据量过大需要分库分表时,跨库 join 基本没法做。大厂通常会先把多表 join 拆成多次单表查询,再在应用层组装,或者把数据同步到分析型存储里做宽表。
这并不是说多表 join 绝对不能用,而是"在分布式、高并发的大规模场景下不划算"。对中小项目,适当的 join 反而更高效、更简单。你如果只有几百张表,每张表几万行,非要去搞微服务化拆分,反而增加了无谓的网络开销和应用层复杂度。
所以在写 JOIN 之前,先问自己三个问题:
- 我是不是真的需要一次性拿到这么多字段?
- 能不能把大表拆小,或者先在应用层做一次过滤?
- 数据量增长之后,这个 JOIN 还能扛得住吗?
2.3 JOIN 优化三件套:索引、驱动表、过滤条件前置
我在实际调优 JOIN 时,核心就三件事:
第一,给关联字段建索引。 JOIN 的 ON 条件里,右侧表(被驱动表)的关联字段必须有索引。常见的坑是在 JOIN 之前已经建过索引,但因为字段类型不一致(比如 char vs varchar),索引直接失效,需要隐式转换才能匹配。另一个坑是字段编码不同,也会导致索引失效。
sql复制-- 注意:user_id 类型是 int,但查询里传了字符串 '123',可能导致索引失效
EXPLAIN SELECT *
