面试频率:★★★★★
工作频率:★★★★★
⚡ 30 秒速记
JOIN 的核心可以先理解成:
先从一张表拿到数据
↓
根据关联字段
↓
去另一张表找对应记录
↓
把两边的数据组合起来
例如:
SELECT o.id, o.amount, u.name
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.status = 1;
可以粗略理解:
orders
↓
先过滤 status = 1
↓
拿到 user_id
↓
根据 user_id
↓
去 user 表找 id
↓
组合结果
JOIN 性能重点关注:
① 驱动表过滤后有多少数据
② 被驱动表关联字段有没有索引
③ WHERE条件有没有合适索引
④ EXPLAIN实际选择了什么执行计划
一句话:
JOIN 优化的核心,是尽量减少参与关联的数据量,并让被驱动表能够通过索引快速找到关联记录。
一、什么是 JOIN?
假设两张表。
用户表:
user
id name
100 张三
200 李四
300 王五
订单表:
orders
id user_id amount
1 100 100
2 200 200
3 100 300
现在想查询:
订单
+
用户姓名
SQL:
SELECT o.id, o.amount, u.name
FROM orders o
JOIN user u
ON o.user_id = u.id;
结果:
订单1 100元 张三
订单2 200元 李四
订单3 300元 张三
这就是:
JOIN
二、JOIN 可以怎么理解?
先不要想复杂算法。
可以简单理解:
orders拿一条数据
id = 1
user_id = 100
↓
根据user_id
↓
去user表查询:
id = 100
↓
找到:
张三
↓
组合:
订单1 + 张三
然后:
orders下一条
user_id = 200
↓
去user表找:
id = 200
↓
李四
不断重复。
所以 JOIN 最大的问题之一就是:
去另一张表找关联数据的时候快不快?
这通常就和:
索引
密切相关。
三、什么是驱动表?
JOIN 执行过程中:
先被读取、用于产生关联数据的一侧
可以先理解成:
驱动表
例如执行计划选择先处理:
orders
然后根据:
orders.user_id
去:
user
查询。
那么可以理解:
orders
↓
驱动表
user
↓
被驱动表
四、什么是被驱动表?
被驱动表:
根据驱动表提供的关联值,被反复查找的表。
例如:
orders
↓
user_id = 100
↓
user
↓
id = 100
这里:
orders
=
驱动表
user
=
被驱动表
流程:
驱动表
↓
拿一条记录
↓
拿到关联字段
↓
查询被驱动表
↓
找到对应记录
五、为什么被驱动表的 JOIN 字段要重点考虑索引?
SQL:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id;
关联条件:
o.user_id = u.id
如果:
u.id
是:
PRIMARY KEY
那么:
u.id
本身有:
主键索引
查询:
id = 100
可以:
通过B+ Tree
↓
快速定位
所以:
orders拿到user_id
↓
user_id = 100
↓
user主键索引
↓
快速找到用户
性能通常比较合理。
六、如果被驱动表关联字段没有索引呢?
假设:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.user_no;
如果:
user.user_no
没有索引。
那么可以粗略理解:
orders拿一条记录
↓
user_id = 100
↓
去user找user_no=100
↓
没有索引
↓
需要扫描大量user数据
然后:
orders下一条
↓
又要去user匹配
如果:
orders
=
10万条
user
=
100万条
这种情况下:
大量驱动记录
+
被驱动表缺少合适索引
JOIN 成本可能非常高。
所以:
JOIN 优化第一反应之一,就是检查被驱动表关联字段有没有合适索引。
七、JOIN 为什么特别依赖索引?
假设驱动表过滤以后:
10000条
那么可以粗略理解:
10000条驱动数据
↓
需要不断去被驱动表寻找对应记录
如果被驱动表:
有索引
每次:
B+ Tree快速定位
如果:
没有合适索引
每次匹配的成本可能明显增加。
所以:
驱动表数据越多
+
被驱动表查找越慢
=
JOIN成本越高
八、哪张表应该做驱动表?
很多教程会直接背:
小表驱动大表
这个说法太简单。
更准确的理解应该是:
尽量让过滤后参与关联的数据量较小。
注意:
不是看原始表有多大
而是看:
WHERE过滤以后
剩多少数据
九、为什么不能死记“小表驱动大表”?
假设:
orders
1000万条
user
100万条
表面看:
user更小
但是 SQL:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.id = 100;
虽然:
orders
有:
1000万条
但是:
WHERE o.id = 100
通过主键以后:
只剩1条
所以:
orders原表很大
并不代表:
过滤后的结果集很大
真正应该考虑:
过滤后的参与关联行数
十、为什么驱动表结果集越小越好?
假设:
驱动表过滤后:
100条
那么:
大约只需要针对这些驱动记录
去被驱动表匹配
如果:
驱动表过滤后:
100万条
那:
关联匹配工作量
通常就会明显增加。
所以:
驱动表过滤后的数据越少
↓
参与JOIN的数据越少
↓
通常越有利于性能
十一、WHERE 和 JOIN 怎么配合索引?
SQL:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.status = 1;
这里有两个重要位置:
WHERE:
o.status = 1
以及:
JOIN:
o.user_id = u.id
可以考虑:
orders.status
↓
帮助过滤数据
user.id
↓
帮助关联查询
所以:
WHERE索引
+
JOIN索引
可以一起优化整个查询。
十二、经典案例
SQL:
SELECT o.id,
o.amount,
u.name
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.create_time >= '2026-09-01';
如果:
orders.create_time
有合适索引:
INDEX(create_time)
可以:
通过create_time
↓
筛选最近订单
↓
得到较少订单
↓
拿到user_id
↓
根据user.id
↓
通过主键索引
↓
找到用户
整个过程:
WHERE
↓
减少驱动数据
JOIN
↓
利用被驱动表索引快速匹配
十三、联合索引怎么参与 JOIN 优化?
实际 SQL:
SELECT o.id,
o.amount,
u.name
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.status = 1;
如果这是非常高频的 SQL,可以根据:
数据分布
查询字段
过滤条件
考虑订单表联合索引。
例如:
INDEX(status, user_id)
可以理解:
status
↓
先过滤
user_id
↓
后续关联需要
但不要死记:
JOIN一定建立(status,user_id)
索引设计还是要结合:
选择性
查询频率
数据量
SELECT字段
其他SQL
最终:
EXPLAIN
验证。
十四、JOIN 和覆盖索引
例如:
SELECT o.user_id, u.name
FROM orders o
JOIN user u
ON o.user_id = u.id
WHERE o.status = 1;
如果订单表有:
(status, user_id)
那么订单这一侧需要:
status
user_id
索引里面都有。
就有机会:
减少额外数据访问
所以之前学的:
覆盖索引
在 JOIN 中同样可能发挥作用。
十五、JOIN 和回表
如果二级索引:
(status, user_id)
但是:
SELECT o.*
需要订单:
所有字段
二级索引里面:
没有完整行
那么:
即使WHERE和JOIN使用了索引
仍可能:
需要回表
所以:
JOIN走索引
≠
完全不回表
还是要区分:
索引定位
和:
覆盖索引
十六、为什么不要随便 SELECT *?
例如:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id;
可能获取:
orders所有字段
+
user所有字段
如果实际上只需要:
订单id
用户姓名
应该:
SELECT o.id, u.name
FROM orders o
JOIN user u
ON o.user_id = u.id;
好处:
减少读取的数据
减少网络传输
减少内存占用
增加覆盖索引的可能性
所以:
不要无脑SELECT *
不仅是代码规范问题。
也可能:
影响SQL性能
十七、JOIN 怎么看 EXPLAIN?
普通 SQL:
EXPLAIN
SELECT *
FROM user
WHERE id = 100;
可能:
一张表
↓
一行主要访问记录
JOIN:
EXPLAIN
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id;
通常会看到:
多行
因为:
需要访问多个表
你可以分析:
哪个表怎么访问
用了哪个索引
预计扫描多少行
关联方式怎么样
十八、JOIN 的 EXPLAIN 重点看什么?
还是你已经熟悉的:
type
key
rows
Extra
type
看:
访问方式
例如:
eq_ref
ref
range
ALL
key
看:
实际使用哪个索引
rows
看:
优化器预计检查多少行
Extra
看:
额外执行信息
所以:
JOIN
并没有重新发明一套 EXPLAIN。
还是:
type
↓
key
↓
rows
↓
Extra
十九、JOIN 中的 eq_ref
你之前学 EXPLAIN 时见过:
eq_ref
JOIN 中非常典型。
例如:
SELECT *
FROM orders o
JOIN user u
ON o.user_id = u.id;
其中:
user.id
是:
PRIMARY KEY
如果执行计划根据:
orders.user_id
去:
user.id
查询。
对于驱动表的一条记录:
最多匹配被驱动表一条记录
这种场景:
eq_ref
就很典型。
可以记:
驱动表一条记录
↓
根据主键 / 唯一索引
↓
被驱动表最多找到一条
↓
eq_ref
二十、JOIN 中的 ref
如果关联字段:
不是唯一索引
例如:
SELECT *
FROM department d
JOIN employee e
ON d.id = e.department_id;
假设:
employee.department_id
有普通索引:
INDEX(department_id)
一个部门:
可能有很多员工
所以:
department_id = 1
可能返回:
100条employee
这种:
非唯一索引等值查询
经常对应:
ref
二十一、eq_ref 和 ref 怎么记?
eq_ref
↓
主键 / 唯一索引关联
↓
对于前面组合的一条记录
最多匹配一条
ref
↓
普通非唯一索引等值匹配
↓
可能匹配多条
简单记:
eq_ref
=
关联过去基本唯一
ref
=
关联过去可能多条
二十二、JOIN 最危险的情况之一
假设:
驱动表过滤后:
100000条
被驱动表:
关联字段没有合适索引
可能出现:
驱动表大量数据
↓
不断匹配被驱动表
↓
被驱动表访问成本很高
↓
JOIN整体成本很高
所以看到:
被驱动表
type = ALL
rows = 很大
就应该重点分析:
关联字段是不是缺少索引?
当然:
ALL
不代表:
100%一定有问题
小表全表扫描:
完全可能很合理
还是结合:
rows
数据量
查询频率
实际耗时
判断。
二十三、INNER JOIN 是什么?
例如:
SELECT *
FROM user u
INNER JOIN orders o
ON u.id = o.user_id;
INNER JOIN:
两边都匹配
↓
才返回
例如:
user:
张三
李四
王五
订单:
张三 → 有订单
李四 → 有订单
王五 → 没订单
INNER JOIN:
张三 ✅
李四 ✅
王五 ❌
因为:
王五没有匹配订单
所以不会返回。
二十四、JOIN 默认就是 INNER JOIN
写:
SELECT *
FROM user u
JOIN orders o
ON u.id = o.user_id;
通常就是:
INNER JOIN
所以:
JOIN
和:
INNER JOIN
在这种语法下语义相同。
二十五、LEFT JOIN 是什么?
例如:
SELECT *
FROM user u
LEFT JOIN orders o
ON u.id = o.user_id;
LEFT JOIN:
左表数据必须保留。
假设:
user:
张三
李四
王五
订单:
张三 → 订单1
李四 → 订单2
王五 → 没订单
结果:
张三 订单1
李四 订单2
王五 NULL
虽然:
王五没有订单
但:
左表user
的数据仍然:
保留
二十六、INNER JOIN 和 LEFT JOIN 区别
INNER JOIN
两边匹配
↓
才返回
LEFT JOIN
左边必须保留
↓
右边没有匹配
↓
补NULL
例如:
user
张三 李四 王五
↓
JOIN orders
INNER JOIN:
张三
李四
LEFT JOIN:
张三
李四
王五 + NULL
二十七、LEFT JOIN 最容易踩的坑
SQL:
SELECT *
FROM user u
LEFT JOIN orders o
ON u.id = o.user_id
WHERE o.status = 1;
注意:
WHERE o.status = 1
是:
对最终结果再次过滤
如果:
王五没有订单
LEFT JOIN 后:
王五
o.status = NULL
然后:
WHERE o.status = 1
判断:
NULL = 1
不成立。
所以:
王五
又被过滤掉了。
二十八、LEFT JOIN + WHERE 为什么容易变得像 INNER JOIN?
原本:
LEFT JOIN
↓
左表没匹配也保留
但是:
WHERE 右表字段 = 某值
又把:
右表为NULL的数据
过滤掉。
结果:
只剩右表匹配成功的数据
表现上就很像:
INNER JOIN
这是非常经典的 SQL 坑。
二十九、条件写 ON 里有什么区别?
例如:
SELECT *
FROM user u
LEFT JOIN orders o
ON u.id = o.user_id
AND o.status = 1;
这里:
o.status = 1
属于:
JOIN匹配条件
可以理解:
user必须保留
↓
只关联status=1的订单
如果:
王五没有status=1订单
仍然:
王五 + NULL
保留。
三十、ON 和 WHERE 在 LEFT JOIN 中的区别
写 ON
LEFT JOIN orders o
ON u.id = o.user_id
AND o.status = 1
意思:
左表全部保留
↓
右表只匹配status=1
写 WHERE
LEFT JOIN orders o
ON u.id = o.user_id
WHERE o.status = 1
意思:
先LEFT JOIN
↓
再过滤最终结果
↓
右表为NULL的记录通常被过滤
所以:
ON条件
和:
WHERE条件
在 LEFT JOIN 中:
不能随便互换
可能直接改变:
查询结果
三十一、JOIN 优化的核心流程
以后看到:
SELECT ...
FROM A
JOIN B
ON A.x = B.x
WHERE ...
按照下面顺序分析。
第一步:看 WHERE
能不能先过滤大量数据?
例如:
1000万
↓
WHERE
↓
100条
非常好。
第二步:看驱动表
过滤后参与JOIN的数据有多少?
不是只看:
原表大小
第三步:看 JOIN 字段
被驱动表关联字段
↓
有没有索引?
第四步:看 SELECT
是不是无脑SELECT *?
能不能:
只查询真正需要的字段
第五步:EXPLAIN
看:
type
key
rows
Extra
确认:
优化器到底怎么执行
三十二、JOIN 优化五条原则
① 尽量减少参与JOIN的数据量
② 被驱动表关联字段建立合适索引
③ WHERE条件尽量提前过滤数据
④ 不要无脑SELECT *
⑤ 使用EXPLAIN验证执行计划
三十三、不要死背“小表驱动大表”
错误:
JOIN优化
=
永远小表驱动大表
正确:
重点看:
过滤后的数据量
+
索引
+
执行计划
+
优化器成本判断
MySQL 优化器会根据:
统计信息
成本估算
可用索引
查询条件
选择执行计划。
所以:
不要为了“手动让小表驱动大表”而乱改 SQL,先看 EXPLAIN。
三十四、和前面的知识串起来
现在你学过:
B+ Tree
↓
索引
↓
联合索引
↓
最左匹配
↓
索引失效
↓
覆盖索引
↓
回表
↓
ICP
↓
EXPLAIN
↓
ORDER BY
↓
GROUP BY
↓
LIMIT深分页
↓
JOIN
JOIN 又把这些东西串起来。
WHERE
索引
↓
减少驱动数据
JOIN
关联字段索引
↓
快速查询被驱动表
SELECT
减少字段
↓
可能形成覆盖索引
↓
减少回表
EXPLAIN
确认:
访问顺序
type
key
rows
Extra
所以整个过程:
SQL
↓
WHERE过滤
↓
驱动表
↓
得到关联字段
↓
被驱动表索引
↓
B+ Tree定位
↓
JOIN结果
↓
SELECT
三十五、一张图记住 JOIN
JOIN
↓
驱动表A
↓
WHERE先过滤
↓
剩余100条参与JOIN
↓
拿关联字段
↓
A.user_id = 100
↓
被驱动表B
↓
JOIN字段有索引?
↙ ↘
有 没有
↓ ↓
B+ Tree定位 高成本匹配
↓ ↓
快速 可能很慢
所以:
JOIN性能
≈
驱动表参与关联的数据量
+
被驱动表查找成本
🎤 面试回答
问:
MySQL JOIN 怎么优化?
答:
JOIN 可以理解为先从驱动表获取数据,再根据关联字段去被驱动表查找匹配记录。因此 JOIN 优化主要关注两个方面:第一是尽量通过 WHERE 等条件减少驱动表参与关联的数据量;第二是保证被驱动表的关联字段有合适索引,让每次关联查询都能够通过 B+ Tree 快速定位。
另外还应该避免无意义的
SELECT *,根据实际查询设计合适的联合索引或覆盖索引,并通过 EXPLAIN 查看各表的 type、key、rows 和 Extra,确认实际执行计划。对于“哪张表做驱动表”,不能简单死记小表驱动大表,更应该关注过滤后的结果集大小以及优化器最终选择的执行计划。
🎯 面试追问
Q1:什么是驱动表?
答:
JOIN执行过程中
先被处理并产生关联数据的一侧
可以简单理解:
驱动表
↓
拿关联字段
↓
查询被驱动表
Q2:什么是被驱动表?
答:
根据驱动表提供的关联值
被查询匹配的表
Q3:JOIN 为什么要给关联字段建索引?
因为:
驱动表产生关联值
↓
需要去被驱动表查询
如果:
被驱动表关联字段有索引
就可以:
B+ Tree快速定位
Q4:为什么不能死记“小表驱动大表”?
因为:
原始表大小
不等于:
过滤后的参与关联数据量
例如:
1000万条的大表
↓
WHERE主键
↓
只剩1条
真正重要的是:
过滤后的结果集
Q5:JOIN 优化主要看什么?
答:
驱动表过滤后的数据量
被驱动表关联字段索引
WHERE索引
SELECT字段
EXPLAIN执行计划
Q6:JOIN 中 eq_ref 是什么?
可以简单记:
根据主键 / 唯一索引关联
↓
对于前面的一条记录
↓
被驱动表最多匹配一条
这种:
eq_ref
非常典型。
Q7:JOIN 中 ref 是什么?
通常:
普通非唯一索引
↓
等值匹配
↓
可能返回多条
例如:
department
↓
很多employee
Q8:eq_ref 和 ref 区别?
eq_ref
↓
唯一匹配
ref
↓
可能匹配多条
Q9:JOIN 走索引以后是不是就不用回表?
不是。
JOIN索引
解决:
关联定位
覆盖索引
解决:
回表
属于不同问题。
Q10:为什么 JOIN 不建议无脑 SELECT *?
因为:
读取更多数据
↓
网络传输更多
↓
内存占用更多
↓
可能失去覆盖索引机会
Q11:INNER JOIN 是什么?
左右两边都匹配
↓
才返回
Q12:LEFT JOIN 是什么?
左表必须保留
右表没有匹配:
NULL
Q13:LEFT JOIN 为什么加 WHERE 可能变成类似 INNER JOIN?
例如:
LEFT JOIN orders o
ON u.id = o.user_id
WHERE o.status = 1
没有订单的用户:
o.status = NULL
然后:
WHERE o.status = 1
把它过滤掉。
所以:
左表没有匹配的数据
最终也没了。
Q14:LEFT JOIN 中 ON 和 WHERE 条件一样吗?
不一样。
ON
↓
决定怎么匹配右表
WHERE
↓
对JOIN后的结果继续过滤
所以:
不能随便互换
Q15:JOIN 怎么使用 EXPLAIN?
执行:
EXPLAIN
SELECT ...
FROM A
JOIN B
ON ...
重点看:
type
key
rows
Extra
并观察:
每张表的访问情况
Q16:JOIN 中看到被驱动表 type=ALL 怎么办?
先看:
rows
如果:
rows很大
就应该重点检查:
关联字段有没有合适索引
如果:
表本身非常小
全表扫描:
也可能完全合理
⚠️ 易错点
1. JOIN字段有索引 ≠ 整条SQL一定快
还要看:
驱动表数据量
WHERE过滤
SELECT字段
回表
执行计划
2. 不要死背“小表驱动大表”
重点:
过滤后的结果集
而不是:
原始表大小
3. type=ALL 不一定绝对有问题
小表:
全表扫描
可能:
成本比走索引还低
所以结合:
rows
实际数据量
判断。
4. JOIN使用索引 ≠ 不回表
JOIN索引
↓
解决关联定位
覆盖索引
↓
解决回表
不要混淆。
5. LEFT JOIN 的 ON 和 WHERE 不能随便换
尤其:
WHERE 右表字段 = ?
可能:
过滤掉右表为NULL的记录
导致:
LEFT JOIN
结果表现得:
类似INNER JOIN
6. 不要为了 JOIN 建一堆重复索引
索引:
会占空间
并且:
INSERT
UPDATE
DELETE
都需要维护索引。
所以:
根据高频SQL
↓
设计合适索引
而不是:
每个字段都建索引
❓ 自测
-
JOIN 是干什么的?
-
JOIN 可以简单理解成什么执行过程?
-
什么是驱动表?
-
什么是被驱动表?
-
为什么被驱动表的关联字段需要重点考虑索引?
-
JOIN 字段没有索引可能出现什么问题?
-
为什么不能简单死记“小表驱动大表”?
-
JOIN 应该看原始表大小还是过滤后的结果集大小?
-
为什么驱动表过滤后的数据越少通常越好?
-
WHERE 条件怎么帮助 JOIN?
-
被驱动表索引怎么帮助 JOIN?
-
ON o.user_id = u.id中,如果u.id是主键有什么好处? -
JOIN 和 B+ Tree 有什么关系?
-
JOIN 和覆盖索引有什么关系?
-
JOIN 走索引以后是不是一定不用回表?
-
为什么不建议无脑
SELECT *? -
EXPLAIN 分析 JOIN 重点看哪些字段?
-
JOIN 为什么在 EXPLAIN 中通常会看到多行?
-
什么是
eq_ref? -
什么是
ref? -
eq_ref和ref最大区别是什么? -
被驱动表
type=ALL是否一定有问题? -
什么是 INNER JOIN?
-
什么是 LEFT JOIN?
-
INNER JOIN 和 LEFT JOIN 最大区别是什么?
-
LEFT JOIN 右表没有数据时返回什么?
-
为什么
LEFT JOIN + WHERE右表字段可能表现得类似 INNER JOIN? -
LEFT JOIN 中 ON 和 WHERE 有什么区别?
-
JOIN 优化的五个核心原则是什么?
-
为什么最终还是要通过 EXPLAIN 验证 JOIN 的执行计划?