Skip to content

JOIN命令

第一部分:到底可不可以使用 JOIN?

一、 背景与核心问题

  1. DBA 为何可能不让使用 JOIN?

  2. 多表 JOIN 时,如何选择驱动表?

二、 MySQL JOIN 的执行算法

  1. 适用场景: 被驱动表的 JOIN 字段上 有可用索引

  2. 执行流程:

    • 从驱动表 t1 逐行读取数据 R。

    • 用 R 中的 JOIN 字段值,通过被驱动表 t2 的索引查找匹配行。

    • 合并结果。

  3. 性能分析:

    • 驱动表全表扫描 (N行)。

    • 被驱动表索引查找 (每次约 2*log₂M 次操作,M为被驱动表行数)。

    • 总复杂度近似:N + N * 2 * log₂M。

  4. 结论1 (能否使用 JOIN): 可以使用。 性能通常优于应用层拆分(减少网络交互和客户端拼接开销)。

  5. 结论2 (驱动表选择): 应选择小表作为驱动表。 (驱动表行数 N 对整体性能影响更大)。

  6. 适用场景: 被驱动表的 JOIN 字段上 无可用索引

  7. 执行流程 (理论):

    • 从驱动表 t1 逐行读取数据 R。

    • 对被驱动表 t2 进行 全表扫描,查找与 R 匹配的行。

  8. 性能分析:

    • 扫描行数:N (驱动表) + N * M (被驱动表扫描 M 次) = N * (1+M)。

    • MySQL 实际并未使用此算法,因其效率极低。

  9. 适用场景: 被驱动表的 JOIN 字段上 无可用索引

  10. 执行流程:

    • 将驱动表 t1 的数据读入 join_buffer (内存)。

    • 扫描被驱动表 t2,逐行与 join_buffer 中的数据进行比较。

    • 满足条件的合并结果。

  11. join_buffer 大小与分块:

    • 由 join_buffer_size 参数控制 (默认 256k)。

    • 若驱动表数据无法一次性放入 join_buffer,则会分块处理:

      1. 驱动表的一部分数据放入 join_buffer。

      2. 扫描整个被驱动表进行匹配。

      3. 清空 join_buffer,放入驱动表下一部分数据。

      4. 再次扫描整个被驱动表进行匹配。

      5. 以此类推,导致被驱动表被多次扫描。

  12. 性能分析 (不分块):

    • 扫描行数:N (驱动表) + M (被驱动表) = N + M。

    • 内存比较次数:N * M。

  13. 性能分析 (分块,驱动表N行,分K段,被驱动表M行):

    • 扫描行数:N + K * M (K ≈ N / (join_buffer_size / 平均行大小))。

    • 内存比较次数:N * M。

  14. 结论1 (能否使用 JOIN): 尽量避免。 扫描行数过多,占用大量系统资源,特别是大表 JOIN。判断依据:EXPLAIN 结果的 Extra 字段出现 "Block Nested Loop"。

  15. 结论2 (驱动表选择): 应选择小表作为驱动表。

    • 当 join_buffer 足够大时,驱动表选择影响不大。

    • 当 join_buffer 不够大时 (常见情况),选择小表作为驱动表可以显著减少被驱动表的扫描次数。

三、 如何定义 "小表"?

  1. 不仅仅是原始表行数。

  2. 准确定义: 两个表按照各自的 WHERE 条件过滤后,实际参与 JOIN 操作的各个字段的总数据量,数据量小的那个表是 "小表"。

    • 考虑因素1:过滤后的行数。

    • 考虑因素2:参与 JOIN 的字段数量和大小。 (需要放入 join_buffer 的数据量)


第二部分:JOIN 语句怎么优化?

一、 Multi-Range Read (MRR) 优化 (优化回表)

  1. 背景: 普通索引查询后的回表操作,如果按索引顺序得到的主键 ID 是随机的,会导致磁盘随机读,性能较差。

  2. MRR 原理:

    • 根据辅助索引定位到满足条件的记录,将主键 ID 放入 read_rnd_buffer。

    • 对 read_rnd_buffer 中的主键 ID 进行排序。

    • 按排序后的主键 ID 顺序到主键索引中查找数据,尽可能将随机读转换为顺序读。

  3. read_rnd_buffer_size: 控制 read_rnd_buffer 的大小。

  4. 启用: set optimizer_switch="mrr_cost_based=off" (强制使用 MRR,而非基于成本判断)。

  5. 核心收益: 适用于范围查询(多值查询),通过排序主键 ID 提升回表效率。

二、 Batched Key Access (BKA) 优化 (NLJ 算法的优化)

  1. NLJ 痛点: 逐行从驱动表取值到被驱动表匹配,无法利用 MRR 的批量优势。

  2. BKA 原理:

    • 从驱动表一次性取出多行数据 (的 JOIN 字段值) 放入 join_buffer。

    • 将 join_buffer 中的这些值批量传给被驱动表。

    • 被驱动表利用 MRR 机制进行优化查找。

  3. 启用: set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; (BKA 依赖 MRR)。

三、 BNL 算法的性能问题与优化

  1. BNL 算法对系统的主要影响:

    • 磁盘 IO 压力: 可能多次扫描被驱动表。

    • CPU 消耗: M*N 次内存比较,大表时非常高。

    • Buffer Pool 污染:

      • 若被驱动表是冷数据表且执行时间超过1秒,多次扫描会将其数据页移入 LRU 链表头部 (young 区域),挤占热数据。

      • 若冷数据表过大,可能导致正常业务的热数据页在 old 区域停留时间不足1秒就被淘汰,无法进入 young 区域。

      • 影响:内存命中率下降,恢复较慢。

  2. 优化方向:将 BNL 转化为 BKA (或 NLJ)。

    • 主要手段:给被驱动表的 JOIN 字段添加索引。
  3. BNL 转 BKA 的临时表方案 (当不适合在原表建索引时):

    • 场景: JOIN 条件涉及的被驱动表数据经过 WHERE 过滤后数据量较小,但原表很大,不宜为低频查询建索引。

    • 思路:

      1. 将被驱动表中满足 WHERE 条件的数据筛选出来,存入一个临时表 temp_t。

      2. 在临时表 temp_t 的 JOIN 字段上创建索引。

      3. 让驱动表与这个带有索引的临时表 temp_t 进行 JOIN (此时会使用 BKA/NLJ)。

    • 效果: 大幅提升性能,减少了不必要的比较次数。

四、 扩展:Hash Join

  1. 概念: 一种更高效的 JOIN 算法,MySQL 目前原生不支持(截至文章撰写时,较新版本如 MySQL 8.0.18+ 已开始支持 Hash Join)。

  2. 原理 (应用层模拟):

    • 将小表 (驱动表) 的数据加载到应用层内存中的哈希表结构 (key 为 JOIN 字段,value 为行数据或所需字段)。

    • 扫描大表 (被驱动表),对每一行数据,用其 JOIN 字段值去哈希表中查找。

    • 匹配成功则合并结果。

  3. 优势: 内存查找效率远高于 BNL 的遍历比较。

  4. 注意: 需要应用层实现,且有内存消耗。

五、 JOIN 优化总结

  1. BKA 优化: MySQL 内置,默认开启,建议使用。

  2. BNL 算法: 效率低下,尽量通过给被驱动表的关联字段加索引转为 BKA/NLJ。

  3. 临时表方案: 适用于能提前大幅过滤被驱动表数据的场景。

  4. Hash Join: (若 MySQL 版本不支持) 可在应用层模拟,通常性能更优。

六、三表 JOIN 的索引设计与连接顺序

  • 语句: select * from t1 join t2 on(t1.a=t2.a) join t3 on (t2.b=t3.b) where t1.c>=X and t2.c>=Y and t3.c>=Z;

  • 索引设计考虑:

    • t1: 考虑在 (a, c) 或 (c, a) 上建联合索引,或单独在 a 和 c 上建索引。

    • t2: 考虑在 (a, b, c) 或其子集上建联合索引,如 (a, c) 和 (b, c) 或 (a,b) (c)。JOIN 字段 a 和 b 很重要。

    • t3: 考虑在 (b, c) 或 (c, b) 上建联合索引,或单独在 b 和 c 上建索引。

    • 目标是让每个 JOIN 操作都能用上被驱动表的索引 (NLJ/BKA),并且 WHERE 条件能有效过滤。

  • straight_join 连接顺序考虑因素:

    • 优先选择过滤后结果集最小的表作为第一个驱动表。

    • 对于后续的 JOIN,依然遵循小表驱动大表的原则(这里的“小表”是上一步 JOIN 结果集与当前表 JOIN 后,参与 JOIN 的数据量)。

    • 确保每一步 JOIN,被驱动表的 JOIN 字段都有索引可用。

    • 例如,如果 t1.c>=X 过滤性最好,可以 t1 作为起始驱动表。然后看 t1 JOIN t2 和 t1 JOIN t3 (如果可以直接 JOIN 的话) 哪个的中间结果集更小,或者哪个能更好地利用后续索引。

    • 一种可能的顺序 (具体取决于数据分布和过滤条件的效果):

      1. t1 (经过 t1.c>=X 过滤)

      2. JOIN t2 ON (t1.a=t2.a) (经过 t2.c>=Y 过滤)

      3. JOIN t3 ON (t2.b=t3.b) (经过 t3.c>=Z 过滤)

    • 需要确保 t2.a 有索引,t3.b 有索引。同时,t2.c 和 t3.c 的条件也要考虑能否利用索引。