聊到MySQL关联查询,很多开发同学第一反应就是“小表驱动大表”,但真正执行起来,驱动表选错了,被驱动表上的索引可能完全发挥不了作用。这并非危言耸听——Index Nested-Loop Join(NLJ)的生效条件相当严格,一旦优化器判断失误,整个JOIN的执行成本会成倍增加。

驱动表选错,被驱动表的索引根本无法生效
MySQL的Index Nested-Loop Join(NLJ)只有在被驱动表的关联字段存在索引时才能生效。如果优化器错误地将大表作为驱动表、小表作为被驱动表,而小表恰好没有建索引——那么整个JOIN就会退化为Block Nested-Loop Join或Hash Join,索引形同虚设。
核心要点在于:索引仅加速“被驱动表”的单行查找,不会加速驱动表的遍历过程。驱动表走全表扫描或范围扫描,被驱动表才依赖索引定位匹配行。因此,即使b.id上建有主键索引,只要b被当作驱动表,该索引在JOIN阶段就完全不会参与匹配逻辑。
- LEFT JOIN中左表固定为驱动表,因此
ON条件里右表的字段必须建有索引 - INNER JOIN中优化器依据WHERE过滤后的结果集大小来选择驱动表,而不是物理表的大小——例如
WHERE status = 'active'后只剩100行的“大表”,也可能被选为驱动表 - 如果连接字段类型不一致(如
INTvsVARCHAR),MySQL会进行隐式转换,导致被驱动表索引失效,即使建立了索引也无法使用
通过EXPLAIN查看驱动顺序,比单纯看表名更可靠
EXPLAIN输出中,id相同且select_type为SIMPLE的行,从上到下即为实际执行顺序:上面的是驱动表,下面的是被驱动表。不要只盯着FROM a JOIN b就认为a是驱动表——优化器可能会重新排列顺序。
重点关注type和Extra字段:
type为ALL或index→ 驱动表正在执行全表扫描或索引扫描type为ref/eq_ref/range→ 被驱动表走了索引查找Extra包含Using join buffer→ 未走索引,触发了Block Nested-Loop JoinExtra包含Using where; Using index→ 被驱动表命中了覆盖索引,效率最高
小表驱动大表 ≠ 物理小表,而是过滤后结果集最小
真正影响NLJ效率的,是驱动表最终需要循环多少次。假设orders表有1000万行,但WHERE created_at > '2026-06-01'后只剩50行;users表只有10万行,但未加WHERE条件,全部参与JOIN。此时优化器大概率会选择orders作为驱动表——外层只循环50次,每次利用user_id索引查询users,总开销远小于反过来。
- 使用
SELECT COUNT(*)配合相同的WHERE条件预估驱动表的结果集大小 - 对驱动表的WHERE条件字段建立索引,可以进一步缩小其扫描范围
- 避免在驱动表上使用
SELECT *,以减少join_buffer的内存压力,尤其是当驱动表意外变大时
被驱动表索引并非建好就万事大吉,还需关注查询路径
即使user_id字段已经建了索引,如果JOIN条件写成ON CAST(o.user_id AS CHAR) = u.id,或者ON o.user_id + 0 = u.id,都会触发隐式转换,导致索引失效。同样,如果被驱动表需要回表(例如SELECT *但索引不是覆盖索引),性能也会打折扣。
- 确保JOIN字段类型完全一致:
TINYINT对TINYINT,VARCHAR(32)对VARCHAR(32),字符集也要相同 - 优先使用主键或唯一索引作为JOIN字段,避免二级索引加回表操作
- 如果被驱动表需要返回大量非索引列,可以考虑添加覆盖索引,例如
INDEX(user_id, name, email)
真正卡住性能的,往往不是“有没有索引”,而是“索引是否在被驱动表上、是否被正确触达”。一次EXPLAIN就能暴露问题,但很多人直接跳过这一步,转头去调join_buffer_size——方向错了,调再久也无济于事。
