MySQL JOIN 机制与多表查询优化:驱动表选择、连接算法与执行计划解析
文章目录
- 《MySQL JOIN 机制与多表查询优化:驱动表选择、连接算法与执行计划解析》
- 一、前言:为什么 JOIN 会变慢?
- 二、JOIN 的底层执行算法:Nested Loop Join(NLJ)
- **① Nested Loop Join(嵌套循环,NLJ)**
- **✔ 实际执行:先拿驱动表,再 JOIN 被驱动表**
- 三、MySQL 如何选择驱动表?
- 判断驱动表的核心原则:
- 四、EXPLAIN 如何判断驱动表?
- **EXPLAIN 中同 id 时,出现顺序表示驱动顺序。**
- 五、JOIN 的三种常见类型与优化建议
- **① INNER JOIN(内连接)**
- **② LEFT JOIN(左连接)**
- **③ RIGHT JOIN(右连接)**
- 六、多表 JOIN(3 张表以上)如何确定顺序?
- **多表 JOIN 优化原则:**
- 七、JOIN 常见的性能杀手(重点)
- 1. JOIN 字段未建索引
- 2. WHERE 条件写在错误的位置
- 3. JOIN 字段⽤了函数
- 4. 多张大表 JOIN
- 八、JOIN 优化的黄金法则
- **✔ 法则 1:小表驱动大表**
- **✔ 法则 2:JOIN 字段必须建索引**
- **✔ 法则 3:WHERE 应尽量放在 JOIN 之前执行**
- **✔ 法则 4:避免 JOIN 字段上使用函数和类型转换**
- **✔ 法则 5:SELECT 只取需要的列**
- **✔ 法则 6:必要时将大表拆分或分库分表**
- 九、面试高频问题与答题模板
- 十、总结
《MySQL JOIN 机制与多表查询优化:驱动表选择、连接算法与执行计划解析》
一、前言:为什么 JOIN 会变慢?
大家好,我是程序员卷卷狗。
JOIN 是业务中最常见的 SQL 操作之一,
但也是最容易写“慢”的 SQL。
常见的性能瓶颈有:
- 驱动表选错导致扫描量激增;
- JOIN 条件未命中索引 → 全表扫描;
- 结果集过大 → 触发临时表、文件排序;
- 多表 JOIN 顺序不合理 → 优化器误判。
JOIN 优化的核心本质就是一句话:
减少参与 JOIN 的数据量。
二、JOIN 的底层执行算法:Nested Loop Join(NLJ)
MySQL 只实现了一种 JOIN 算法:
① Nested Loop Join(嵌套循环,NLJ)
伪代码如下:
for 被驱动表的每一行 (outer)for 驱动表中满足条件的行 (inner)输出两表匹配的行
但实际执行顺序与概念相反:
✔ 实际执行:先拿驱动表,再 JOIN 被驱动表
官方定义:
- 驱动表(Driving Table):最先读取的表
- 被驱动表(Driven Table):根据驱动表结果继续匹配的表
图示:
驱动表 records1 × 被驱动表 records2
-------------------------------------
最终 JOIN = records1 * records2 的匹配集合
为了保证性能:
驱动表必须尽可能小,被驱动表必须命中索引。
三、MySQL 如何选择驱动表?
MySQL 的驱动表选择完全依赖优化器(Optimizer)。
判断驱动表的核心原则:
| 原则 | 解释 |
|---|---|
| 1. WHERE 过滤后数据量最小的表优先 | 越小的表做驱动表,循环次数越少 |
| 2. ON 条件能命中索引的表优先作为被驱动表 | JOIN 时必须命中索引 |
| 3. EXPLAIN 中 id 值大者先执行 | id 越大,越先执行(驱动表) |
| 4. type 趋向 ref、const 的表优先 | 避免 range、ALL |
举例:
SELECT *
FROM user u
JOIN order o ON u.id = o.user_id
WHERE u.age > 18;
驱动表:user(u)
被驱动表:order(o)
因为:
- user 先过滤 age > 18 → 数据量小;
- order.user_id 上有索引 → 可直接匹配。
四、EXPLAIN 如何判断驱动表?
示例:
EXPLAIN
SELECT * FROM user u
JOIN orders o ON u.id = o.user_id;
可能输出(简化版):
| id | table | type | key |
|---|---|---|---|
| 1 | user | ALL | NULL |
| 1 | orders | ref | idx_user_id |
解释:
- 两行 id 都是 1 → 同一层 JOIN
- 顺序:上面的先执行 → user 为驱动表
- 下表 orders 用 ref 索引匹配 → 被驱动表
记住:
EXPLAIN 中同 id 时,出现顺序表示驱动顺序。
五、JOIN 的三种常见类型与优化建议
① INNER JOIN(内连接)
SELECT * FROM A JOIN B ON A.id=B.id
优化:
- JOIN 条件字段必须有索引
- 小表驱动大表
- WHERE 先过滤大数据量的表
② LEFT JOIN(左连接)
SELECT * FROM A LEFT JOIN B ON A.id=B.id
注意:A 永远是驱动表,不能被优化器调整。
优化:
- B.id 必须有索引
- WHERE 条件避免过滤 B(避免转 INNER JOIN)
- 避免在 B 表 JOIN 字段使用函数
③ RIGHT JOIN(右连接)
不建议使用 RIGHT JOIN,
因为它完全等价于 LEFT JOIN 翻转顺序,
但会降低可读性。
六、多表 JOIN(3 张表以上)如何确定顺序?
举例:
A JOIN B ON A.id = B.a_id
JOIN C ON B.id = C.b_id
执行顺序通常是:
1. A JOIN B
2. (A+B) JOIN C
优化器会根据过滤条件、索引、统计信息决定每一步的驱动与被驱动表。
多表 JOIN 优化原则:
| 原则 | 解释 |
|---|---|
| 1. 永远让过滤后行数最少的表靠前 | 减少循环次数 |
| 2. 每一步 JOIN 的 ON 字段必须命中索引 | 否则全表扫描 |
| 3. 避免在 JOIN 字段上使用函数或类型转换 | 避免索引失效 |
| *4. 避免使用 SELECT ,减少网络与磁盘开销 | 提升吞吐 |
| 5. 多表 JOIN 尽量少于 3 张表 | 否则成本极高 |
七、JOIN 常见的性能杀手(重点)
1. JOIN 字段未建索引
SELECT * FROM user u JOIN order o ON u.id=o.user_id;
若 o.user_id 无索引 → 全表扫描。
2. WHERE 条件写在错误的位置
SELECT *
FROM user u
LEFT JOIN orders o ON u.id=o.user_id
WHERE o.user_id IS NOT NULL;
→ LEFT JOIN 被转成 INNER JOIN。
3. JOIN 字段⽤了函数
ON DATE(u.create_time) = DATE(o.create_time)
索引全部失效。
4. 多张大表 JOIN
JOIN 是 N² 的复杂度,
表越大,损耗呈指数级增长。
八、JOIN 优化的黄金法则
✔ 法则 1:小表驱动大表
减少嵌套循环的迭代次数。
✔ 法则 2:JOIN 字段必须建索引
尤其是被驱动表的 JOIN 字段。
✔ 法则 3:WHERE 应尽量放在 JOIN 之前执行
过滤越早越好。
✔ 法则 4:避免 JOIN 字段上使用函数和类型转换
必须让索引“裸奔”。
✔ 法则 5:SELECT 只取需要的列
减少数据传输量。
✔ 法则 6:必要时将大表拆分或分库分表
降低单表数据量。
九、面试高频问题与答题模板
| 问题 | 答案要点 |
|---|---|
| Q1:MySQL JOIN 底层使用什么算法? | Nested Loop Join(嵌套循环)。 |
| Q2:什么是驱动表?怎么选? | 最先读取的表,通常是过滤后较小的表。 |
| Q3:JOIN 为什么会变慢? | 扫描量大、未命中索引、临时表或文件排序。 |
| Q4:如何优化多表 JOIN? | 小表驱动大表、JOIN 字段建索引、避免函数。 |
| Q5:LEFT JOIN 中谁是驱动表? | 左表(永远是驱动表)。 |
| Q6:如何通过 EXPLAIN 判断驱动表? | 看 id 相同的多行中出现顺序,越上者越先执行。 |
| Q7:为什么 JOIN 字段必须建索引? | 提升匹配效率,否则每次匹配都全表扫描。 |
十、总结
JOIN 是 SQL 中最复杂也最容易写痛点的部分,
其性能完全取决于:
- 驱动表选择
- 索引命中
- JOIN 条件写法
- Where 过滤顺序
- 表数据量
一句话记住:
小表驱动大表,被驱动表靠索引。
下一篇(第 21 篇),我将写——
《MySQL缓存机制与查询缓存的消亡史》,
讲清楚优化器、执行器、存储引擎之间的协作与下推机制。
