mysql执行SQL时join操作的执行顺序_mysql多表关联执行解析

1次阅读

mysql的join执行顺序由优化器基于统计信息决定,并非按sql书写顺序;left join中on与where语义不同,错放条件会导致连接退化为inner join;驱动表应小且过滤性强,被驱动表on字段需有高效索引。

mysql执行SQL时join操作的执行顺序_mysql多表关联执行解析

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 ...
  • 查看真实计划时,注意 rowsfiltered 列:前者是预估扫描行数,后者是条件过滤后剩余比例,两者相乘接近实际参与 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' —— 这条语句会排除所有 usersNULL 的订单(即无用户信息的订单),因为 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。

  • EXPLAINtype 列:被驱动表显示 ALL 就危险;理想是 refeq_refrange
  • 被驱动表的 ON 字段必须是索引的最左前缀,比如索引是 (a, b),那么 ON t1.a = t2.a 可用,但 ON t1.b = t2.b 不可用
  • 临时表场景下(如子查询物化),驱动表也可能变成内存临时表,此时要注意 tmp_table_sizemax_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 = ALLindex);一旦走了索引,join_buffer 就不参与
  • 每个被驱动表单独分配一个 buffer,三表 join 且都无索引时,最多消耗 3 × join_buffer_size
  • 线上调优优先建索引,而不是盲目调大 join_buffer_size;buffer 再大也救不了没索引的 NLJ

实际执行路径远比语法结构复杂,尤其当统计信息不准、索引失效或存在隐式类型转换时,优化器很容易选错驱动表或忽略可用索引。别只信 EXPLAIN 的预估,务必用 EXPLAIN format=json 查看 used_columnskey_lengthrows_examined_per_scan 这些细节。

text=ZqhQzanResources