where条件必须写在join之前,单表过滤条件应在扫描该表时执行以减少io;left join中where会转为inner join,需用子查询提前剪枝;join字段类型必须一致,否则索引失效;小表驱动由explain的rows决定而非书写顺序。

WHERE条件必须写在JOIN之前,而不是堆在最后
很多人把所有过滤条件都塞进最终的WHERE子句,结果发现EXPLAIN里rows_examined高得离谱。这不是因为JOIN慢,而是数据库被迫先读完几十万行再筛——IO全花在“读”上了。
关键判断:单表可独立判断的条件(比如orders.status = 'shipped'、users.deleted = 0),必须让优化器在扫描该表时就执行,而不是等JOIN完再过滤。
- 正确写法:
SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'shipped' AND u.status = 'active'→ 两张表各自按条件扫描,数据量从10万→几百 - 错误写法:
SELECT * FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status = 'shipped' AND u.status = 'active'→ 看似一样,但若优化器没下推,orders可能全扫 - LEFT JOIN要特别小心:
WHERE u.deleted = 0会把左连接变成内连接;真要保留左表全部,得用子查询提前剪枝
用子查询或CTE显式限定大表参与JOIN的行数
当某张表数据量极大(比如日志表、订单明细),又只用其中一小部分做关联时,硬JOIN等于主动邀请IO爆炸。显式子查询是唯一可控的提前剪枝方式。
它不依赖优化器是否“聪明”,只要子查询里字段有索引,就能把扫描行数压到最低。
- 坏例子:
SELECT u.name, l.ip FROM users u JOIN log l ON u.id = l.user_id WHERE l.created_at > '2026-07-01'→ log表全扫,哪怕加了created_at索引也可能因类型不匹配失效 - 好例子:
SELECT u.name, l.ip FROM users u JOIN (SELECT user_id, ip FROM log WHERE created_at > '2026-07-01') l ON u.id = l.user_id→ log只查目标日期范围,且子查询中user_id和created_at必须有复合索引 - 注意:子查询别名不能省,MySQL 5.7+对无别名的子查询支持不稳定;CTE写法更清晰,但MySQL 8.0+才完全支持
JOIN字段类型不一致,再早的过滤也救不了IO
这是最常被忽略的硬伤。哪怕你把WHERE写得再精准,只要ON两边字段类型不同(比如INT vs VARCHAR),MySQL就会放弃走索引,直接全表扫描——这时“减少数据量”全是空谈。
查EXPLAIN时重点看两处:type是不是ALL或index,Extra有没有Using join buffer或Using where但没Using index。
- 确认方法:
SHOW CREATE TABLE log和SHOW CREATE TABLE users,比对user_id字段类型、字符集、是否允许NULL - 修复优先级:改表结构 > 用
CAST()临时转换(如ON u.id = CAST(l.user_id AS SIGNED))> 加索引(无效) - 典型场景:宽表导出、日志系统接入、跨库同步后字段定义漂移
小表驱动大表不是靠写序决定的,得看EXPLAIN里的rows
很多人以为把小表写在FROM后面就是“小表驱动”,其实MySQL优化器会重排顺序。真正决定性能的是哪张表被当作驱动表——也就是EXPLAIN输出中rows值最小的那张。
如果发现大表成了驱动表(比如orders的rows是50万,users只有1千),说明WHERE没生效、索引没建对,或者类型不一致导致优化器误判。
- 强制小表驱动可用
STRAIGHT_JOIN,但仅限调试验证,上线前必须确认执行计划稳定 - INNER JOIN比LEFT JOIN更容易触发小表驱动,因为优化器不用保全左表所有行
- 如果业务允许,把LEFT JOIN改成INNER JOIN,有时能直接让优化器选对驱动表
真正卡住性能的从来不是JOIN逻辑本身,而是没过滤就让数据库去读那些根本不会出现在最终结果里的行。每多读一万行,IO、内存、网络传输就多扛一份压力——而这些压力,在EXPLAIN里只体现为一个rows数字。











