MySQL 的 JOIN 执行顺序由优化器决定,不是按 SQL 书写顺序
写
SELECT * FROM A JOIN B ON ... JOIN C ON ...,不代表 MySQL 先连 A 和 B、再连 C。优化器会基于统计信息(如行数、索引选择性)重排表连接顺序,目标是让中间结果集最小。你可以用
EXPLAIN看实际顺序:
EXPLAIN SELECT ...输出的
table列从上到下就是物理执行顺序。
常见误解是“左连接就一定先查左边”,其实
LEFT JOIN仅保证左表全量保留,但优化器仍可能把右表提前物化或下推条件——只要语义等价,它就敢改。 强制固定顺序?加
STRAIGHT_JOIN(仅限
INNER JOIN场景),例如:
SELECT STRAIGHT_JOIN * FROM A JOIN B ON ... JOIN C ON ...查看真实计划时,注意
rows和
filtered列:前者是预估扫描行数,后者是条件过滤后剩余比例,两者相乘接近实际参与 join 的行数 没走索引的 ON 条件会导致嵌套循环(NLJ)退化为全表扫描级联,比如
A.id = B.non_indexed_col可能让 B 被扫几十次
ON 和 WHERE 对 LEFT JOIN 的影响完全不同
ON是在 join 过程中决定“哪些右表行能匹配左表行”,而
WHERE是在 join 完成后对整行结果过滤。对
LEFT JOIN来说,把本该写在
ON里的条件错放
WHERE,会直接让左连接退化为内连接。
例如:
SELECT * FROM orders LEFT JOIN users ON orders.user_id = users.id WHERE users.status = 'active'—— 这条语句会排除所有
users为 NULL 的订单(即无用户信息的订单),因为
WHERE在 join 后执行,
NULL = 'active'为 false。 正确写法应是:
SELECT * FROM orders LEFT JOIN users ON orders.user_id = users.id AND users.status = 'active'如果是多表 LEFT JOIN,每个
ON只约束紧邻右侧的表,不跨表生效 复合条件如
ON a.x = b.y AND b.z > 10中,
b.z > 10属于“join 条件”,会在 join 阶段过滤 b 表,而非最后扫全量再筛
驱动表与被驱动表的选择直接影响性能
MySQL 的 Nested Loop Join(NLJ)里,先查的叫驱动表(outer table),后查的叫被驱动表(inner table)。驱动表应尽量小且有高选择性过滤条件;被驱动表必须对 ON 字段有高效索引,否则每行驱动都触发一次全表扫描。
例如:10 万行的
log表 LEFT JOIN 100 行的
config表,即使
config没索引也问题不大;但如果反过来,
config做驱动表去连百万行
log,而
log.event_type没索引,就会执行 100 × 百万次磁盘 I/O。 用
EXPLAIN看
type列:被驱动表显示
ALL就危险;理想是
ref、
eq_ref或
range被驱动表的 ON 字段必须是索引的最左前缀,比如索引是
(a, b),那么
ON t1.a = t2.a可用,但
ON t1.b = t2.b不可用 临时表场景下(如子查询物化),驱动表也可能变成内存临时表,此时要注意
tmp_table_size和
max_heap_table_size是否够用,否则会落盘变慢
JOIN 缓存(join_buffer)只用于非驱动表的关联字段匹配
join_buffer是线程级内存缓冲区,用于加速被驱动表的查找。它不缓存驱动表数据,也不缓存被驱动表整行,只缓存 ON 条件中涉及的字段(如
ON a.id = b.a_id,则只缓存
b.a_id)。
这个缓冲区大小由
join_buffer_size控制,默认仅 256KB。当被驱动表很大、又无法走索引时,增大它可减少磁盘 I/O,但过大会挤占其他操作内存。 只对未使用索引的 join 有效(
type = ALL或
index);一旦走了索引,
join_buffer就不参与 每个被驱动表单独分配一个 buffer,三表 join 且都无索引时,最多消耗 3 ×
join_buffer_size线上调优优先建索引,而不是盲目调大
join_buffer_size;buffer 再大也救不了没索引的 NLJ
实际执行路径远比语法结构复杂,尤其当统计信息不准、索引失效或存在隐式类型转换时,优化器很容易选错驱动表或忽略可用索引。别只信
EXPLAIN的预估,务必用
EXPLAIN FORMAT=JSON查看
used_columns、
key_length和
rows_examined_per_scan这些细节。
