mysql join扫描行数超预期是因为驱动表逐行访问被驱动表,若后者未走索引则全表扫描;关键看explain的type字段是否为all/index,需确保on条件列有匹配索引、无函数操作、字符集一致。

JOIN 执行时为什么扫描行数远超预期
MySQL 的 JOIN 不是先算完左表再匹配右表,而是按驱动表(通常是 EXPLAIN 中 id 最小且没依赖其他表的那张)逐行取值,对每行去被驱动表查匹配项。如果被驱动表没走索引,就会全表扫描——10 万行驱动表 × 50 万行被驱动表 = 500 亿行扫描,不是夸张,是真实发生过的线上事故。
关键判断点:看 EXPLAIN 输出里的 type 字段。如果是 ALL 或 index,基本等于宣告被驱动表正在全扫描。
- 确保
ON或USING条件中的列,在被驱动表上有合适的单列索引或联合索引(注意最左前缀) - 避免在
ON子句里对被驱动表字段做函数操作,比如ON YEAR(t2.create_time) = YEAR(t1.year)会让索引失效 - 检查字符集和排序规则是否一致,
t1.name是utf8mb4_0900_as_cs而t2.name是utf8mb4_general_ci,可能导致隐式转换、索引失效
用 STRAIGHT_JOIN 强制驱动表顺序真的安全吗
默认情况下优化器会选它认为“代价最小”的驱动表,但它的统计信息可能过期,或对复杂条件预估不准。这时手动指定 STRAIGHT_JOIN 可跳过优化器决策,直接按 FROM 后顺序执行。
但它不是银弹:一旦业务数据分布变化(比如某字段高基数变低基数),原来最优的驱动顺序可能变成最差。线上曾有案例,加了 STRAIGHT_JOIN 后 QPS 掉 70%,因为驱动表从 1 万行涨到 80 万行,而被驱动表索引刚好因字段类型不一致没生效。
- 只在
EXPLAIN明确显示优化器选错驱动表,且你已验证新顺序在当前数据量下更优时使用 - 必须配合
ANALYZE TABLE更新统计信息,并定期复查 - 禁止在没有
WHERE过滤的多表JOIN中滥用,容易触发笛卡尔积放大
如何让 JOIN 只扫被驱动表的必要索引 B+ 树节点
理想情况是:驱动表每行只触发一次索引查找(ref 或 eq_ref),而不是扫描整个索引树。这要求被驱动表的连接字段不仅是索引前缀,还要满足“等值查询 + 无 NULL 值 + 类型严格匹配”。
例如 t2.status 上有索引,但 ON t1.status = t2.status 中,若 t2.status 允许为 NULL,MySQL 在某些版本中可能退化为 range 访问,甚至放弃使用该索引。
- 连接字段设为
NOT NULL,并确保实际数据无空值 - 联合索引要覆盖
ON条件全部列,且顺序与ON中出现顺序一致(如ON t1.a=t2.a AND t1.b=t2.b,则索引应为(a,b),不是(b,a)) - 对大表 JOIN,考虑用
IN (SELECT ...)替代JOIN—— 如果子查询结果集小且能走主键,有时比嵌套循环更省 I/O
SELECT /*+ USE_INDEX(t2, idx_status_type) */ t1.id, t2.name FROM orders t1 JOIN order_items t2 ON t1.id = t2.order_id WHERE t1.created_at > '2024-01-01' AND t2.status = 'shipped';
临时表/派生表 JOIN 时的扫描陷阱
当 JOIN 中包含子查询(如 (SELECT ... ) AS tmp),MySQL 5.7+ 默认将结果物化为内部临时表,且该表默认无索引。后续若拿这个临时表去 JOIN,很可能触发全表扫描。
解决方法不是加索引(内部临时表不支持),而是改写逻辑:把子查询上拉成 JOIN,或用 CTE(MySQL 8.0+)配合 MATERIALIZED 提示控制物化时机。
- 用
EXPLAIN FORMAT=TREE查看是否出现<materialize></materialize>节点 - 对简单聚合子查询,尝试用
LEFT JOIN+GROUP BY重写,避免物化 - 确认 MySQL 版本是否支持
/*+ NO_MERGE() */提示,防止优化器把子查询合并进外层,反而打乱你的索引计划
EXPLAIN 的 rows 和 key_len 里抠出来算。










