qinyelin
发布于 2026-09-10 / 4 阅读
0
0

MySQL JOIN 原理与索引优化

面试频率:★★★★★
工作频率:★★★★★


⚡ 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
↓
设计合适索引

而不是:

每个字段都建索引

❓ 自测

  1. JOIN 是干什么的?

  2. JOIN 可以简单理解成什么执行过程?

  3. 什么是驱动表?

  4. 什么是被驱动表?

  5. 为什么被驱动表的关联字段需要重点考虑索引?

  6. JOIN 字段没有索引可能出现什么问题?

  7. 为什么不能简单死记“小表驱动大表”?

  8. JOIN 应该看原始表大小还是过滤后的结果集大小?

  9. 为什么驱动表过滤后的数据越少通常越好?

  10. WHERE 条件怎么帮助 JOIN?

  11. 被驱动表索引怎么帮助 JOIN?

  12. ON o.user_id = u.id 中,如果 u.id 是主键有什么好处?

  13. JOIN 和 B+ Tree 有什么关系?

  14. JOIN 和覆盖索引有什么关系?

  15. JOIN 走索引以后是不是一定不用回表?

  16. 为什么不建议无脑 SELECT *

  17. EXPLAIN 分析 JOIN 重点看哪些字段?

  18. JOIN 为什么在 EXPLAIN 中通常会看到多行?

  19. 什么是 eq_ref

  20. 什么是 ref

  21. eq_refref 最大区别是什么?

  22. 被驱动表 type=ALL 是否一定有问题?

  23. 什么是 INNER JOIN?

  24. 什么是 LEFT JOIN?

  25. INNER JOIN 和 LEFT JOIN 最大区别是什么?

  26. LEFT JOIN 右表没有数据时返回什么?

  27. 为什么 LEFT JOIN + WHERE右表字段 可能表现得类似 INNER JOIN?

  28. LEFT JOIN 中 ON 和 WHERE 有什么区别?

  29. JOIN 优化的五个核心原则是什么?

  30. 为什么最终还是要通过 EXPLAIN 验证 JOIN 的执行计划?


评论