加索引后JOIN仍慢,主因是NLJ未正确使用索引:驱动表选错、JOIN字段无索引或不匹配、复合索引顺序错误、字符集不一致等导致被驱动表全表扫描;需通过EXPLAIN的type列判断,并确保被驱动表JOIN字段命中索引最左前缀。

为什么加了索引,JOIN 还慢得像没加?
因为 MySQL 的 Nested Loop Join(NLJ)默认按驱动表的顺序逐行扫描被驱动表——如果被驱动表上没用上索引,或者用了但不是覆盖索引,就会变成全表扫描。哪怕驱动表只有 10 行,被驱动表 100 万行,也可能触发 10 × 100 万次随机 I/O。
关键判断点:EXPLAIN 输出里看 type 列:如果是 ALL 或 index,基本就是 NLJ 没走对索引;如果是 ref、eq_ref 或 range,说明索引用上了。
- 驱动表选错:小表没当驱动表,大表反而成了外层循环
- 被驱动表 JOIN 条件字段没建索引,或索引不匹配(比如用了函数、类型隐式转换)
- 复合索引顺序不对:比如
ON t1.a = t2.x AND t1.b = t2.y,但 t2 上只建了(y, x)而不是(x, y) - 字符集/排序规则不一致,导致索引失效(常见于跨库 JOIN 或旧表迁移后)
如何让 NLJ 真正走索引?
核心是让被驱动表的 JOIN 字段能命中 B+ 树的最左前缀。MySQL 5.7+ 默认启用 BNL(Block Nested-Loop)优化,但前提是被驱动表能走索引;否则它会退化成 BNL + 全表扫描,更耗内存。
- 确保被驱动表的 ON 条件字段有单列索引,或复合索引以该字段为第一列
- 避免在 JOIN 字段上做计算:
ON t1.id = t2.id + 1会让t2.id索引失效 - 检查字段类型是否严格一致:
INT和BIGINT关联可能触发隐式转换,VARCHAR和CHAR也要留意尾部空格处理 - 用
SHOW CREATE TABLE确认索引实际定义,别只信命名习惯(比如叫idx_user_id却建在order_id上)
示例:如果执行 SELECT * FROM orders o JOIN users u ON o.user_id = u.id,那么 users.id 必须是主键或有唯一索引,orders.user_id 建普通索引即可——但反过来,如果 orders 是被驱动表,user_id 就必须有索引。
EXPLAIN 里看到 type=ALL,但明明建了索引?
大概率是索引没被选中,而不是没建。MySQL 优化器会基于统计信息估算成本,有时宁可全表扫描也不走索引(比如返回行数预估 > 20% 总行数)。
- 执行
ANALYZE TABLE更新统计信息,尤其在大批量 INSERT/DELETE 后 - 用
FORCE INDEX强制走某索引(慎用,上线前必须压测):JOIN users FORCE INDEX (PRIMARY) ON ... - 检查 WHERE 条件是否“污染”了索引选择:比如
WHERE u.status = 1 AND o.created_at > '2024-01-01'可能让优化器放弃u.id索引 - 注意
STRAIGHT_JOIN:它能固定驱动表顺序,配合索引使用比依赖优化器更可控
多表 JOIN 时索引怎么配才不翻车?
每多一个 JOIN,被驱动表就多一层嵌套循环。三层 JOIN 下,如果第二层和第三层都没走索引,性能是指数级恶化。
- 从
EXPLAIN的table列顺序反推驱动顺序,优先保证最内层被驱动表有强索引 - 复合索引要覆盖 JOIN + WHERE + SELECT 需要的字段,减少回表(例如
(user_id, status, created_at)支持ON和WHERE status=1) - 避免在 JOIN 字段上用
LIKE '%xxx'或IS NULL,这些无法利用 B+ 树的有序性 - 分区表 JOIN 要特别小心:跨分区查询可能让局部索引失效,
EXPLAIN PARTITIONS比普通 EXPLAIN 更有用
真正难的不是建索引,是建对那个索引——它得同时满足驱动顺序、字段顺序、数据分布、查询模式四个条件。少一个,NLJ 就可能默默退化成全表扫。










