右表未命中仍慢,是因为MySQL需为左表每行执行一次右表查找;若右表ON字段无索引、类型不一致或统计不准,将引发全表扫描或隐式转换,导致rows暴增、filtered骤降,查询开销超90%。

LEFT JOIN右表未命中时,MySQL仍要为左表每一行执行一次右表查找——如果右表没索引、字段类型不一致或统计信息不准,这个“空跑”开销会吃掉90%以上查询时间。
为什么右表未命中也慢?
LEFT JOIN语义要求保留左表所有行,即使右表完全没匹配。但MySQL执行时,对左表每行都会触发一次右表的查找动作。如果右表ON字段没索引,就会变成全表扫描;哪怕有索引,若字段类型不一致(比如左表id是BIGINT,右表ref_id是VARCHAR),也会触发隐式转换导致索引失效——此时EXPLAIN里key列可能还显示索引名,但rows暴增、filtered掉到10%,本质仍是逐行比对。
常见错误现象:
-
EXPLAIN中右表type为ALL或index,Extra含Using join buffer (Block Nested Loop) - 左表只返回几百行,但查询耗时数秒,
SHOW PROFILE显示大量Creating sort index或Copying to tmp table - 加了
WHERE b.status = 'done'后反而更慢——这实际把LEFT JOIN变成了INNER JOIN,但优化器没识别出来
右表ON字段必须单独建索引,且类型严格一致
索引必须建在右表的ON字段上,不是左表。左表驱动,右表被查,索引建错方向等于白建。
实操建议:
- 用
ALTER TABLE right_table ADD INDEX idx_join_field (join_field);建单列索引,90%场景已够用 - 如果右表还有
WHERE条件(如status = 'active'),建联合索引要把join_field放最左:ADD INDEX idx_join_status (join_field, status) - 检查字段类型是否完全一致:
DESCRIBE left_table和DESCRIBE right_table对比join_field的Type、Collation,不一致就用ALTER TABLE统一 - 避免在
ON里用函数:ON a.id = CAST(b.ref_id AS SIGNED)会让索引彻底失效
提前过滤右表,别让它全量参与JOIN
即使右表有索引,如果它本身数据量极大(比如千万级订单表),每次查找仍需定位、读取、判断——不如先砍掉95%无效数据。
实操建议:
- 把右表
WHERE条件挪进子查询:LEFT JOIN (SELECT * FROM orders WHERE create_time > '2025-01-01') o ON u.id = o.user_id - 如果右表过滤条件复杂,考虑用临时表缓存结果,再JOIN临时表
- 警惕
WHERE b.field IS NOT NULL这类写法——它让LEFT JOIN退化为INNER JOIN,但优化器可能仍按LEFT逻辑执行,白白多扫一遍右表 - 想保留左表空匹配又需右表满足条件?把条件写进
ON子句:LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'done'
强制驱动顺序与覆盖索引能压榨最后10%性能
当左表本身也很大,且过滤性不强时,优化器可能选错驱动表;而覆盖索引能避免回表,减少I/O。
实操建议:
- 确认左表确实比右表小(或过滤后更小),再用
STRAIGHT_JOIN强制顺序:SELECT STRAIGHT_JOIN u.name, o.amount FROM users u LEFT JOIN orders o ON u.id = o.user_id - 为常用查询字段建覆盖索引:比如查
u.name, o.amount,就在users上建INDEX idx_dept_id_name (dept, id, name),在orders上建INDEX idx_user_id_amount (user_id, amount) - 避免
SELECT *,只查真正需要的字段——尤其右表字段多时,NULL填充+网络传输开销不可忽视 - 对线上慢查询,优先用
EXPLAIN FORMAT=TREE(MySQL 8.0+)看真实执行树,别信自己写的表顺序
最容易被忽略的是字段类型和字符集的一致性——哪怕只差一个utf8mb4_unicode_ci和utf8mb4_general_ci,索引也可能失效。每次加索引前,先SHOW CREATE TABLE比对两张表的定义。


















