驱动表选错会导致被驱动表索引失效;NLJ仅在被驱动表关联字段有索引时生效,索引只加速被驱动表单行查找,不加速驱动表遍历。
MySQL的Index Nested-Loop Join(NLJ)只在被驱动表的关联字段有索引时才生效。如果优化器误把大表当驱动表、小表当被驱动表,而小表恰好没建索引——那整个JOIN就退化成Block Nested-Loop Join或Hash Join,索引形同虚设。
关键点在于:**索引只加速“被驱动表”的单行查找,不加速驱动表的遍历**。驱动表走全表扫描或范围扫描,被驱动表才靠索引定位匹配行。所以哪怕b.id上有主键索引,只要b被当成驱动表,这个索引在JOIN阶段就完全不会参与匹配逻辑。
ON条件里右表的字段必须有索引WHERE status = 'active'后只剩100行的“大表”,也可能被选为驱动表INT vs VARCHAR),MySQL会隐式转换,导致被驱动表索引失效,即使建了也没用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 Join
Extra含Using where; Using index → 被驱动表命中覆盖索引,效率最高真正影响NLJ效率的,是驱动表最终要循环多少次。假设orders有1000万行,但WHERE created_at > '2026-06-01'后只剩50行;users只有10万行,但没加WHERE,全量参与。这时优化器大概率选orders当驱动表——外层只循环50次,每次用user_id索引查users,总开销远小于反过来。
SELECT COUNT(*)配合相同WHERE条件预估驱动表结果集大小SELECT *,减少join_buffer内存压力,尤其当它意外变大时即使user_id字段建了索引,如果JOIN条件写成ON CAST(o.user_id AS CHAR) = u.id,或者ON o.user_id + 0 = u.id,都会触发隐式转换,索引失效。同样,如果被驱动表需要回表(比如SELECT *但索引不是覆盖索引),性能也会打折扣。
TINYINT对TINYINT,VARCHAR(32)对VARCHAR(32),字符集也要相同INDEX(user_id, name, email)
EXPLAIN就能暴露问题,但很多人直接跳过这步,转头去调join_buffer_size——方向错了,调再久也没用。