MySQL 多表关联执行计划全面解析:从 N-LJ 到子查询优化

在实际企业开发中,多表关联查询更为常见,也是导致 SQL 执行效率低下的重要原因之一。

今天,我们将系统性地解析 MySQL 多表关联查询的执行机制,重点包括:

  1. 多表关联底层执行机制(N-LJ 嵌套循环连接)
  2. 为什么多表查询容易性能差,以及驱动表的选择有多重要
  3. 多表查询执行计划分析与优化技巧(附实际案例)

一、MySQL 多表关联的执行机制:N-LJ 嵌套循环连接

在 MySQL 中,多表连接最常见的执行策略就是 Nested Loop Join(N-LJ),也称为嵌套循环连接。这个概念并不复杂,本质上就是外层循环一行一行遍历主表(驱动表),每一行再去内部表中找匹配记录

1.1 N-LJ 的过程示意

以如下两张表为例:

  • emp:员工表(emp_id, name)
  • salary:员工工资表(emp_id, amount)

查询语句:

SELECT e.emp_id, s.amount 
FROM emp e 
JOIN salary s ON e.emp_id = s.emp_id 
WHERE e.emp_id > 1;

执行过程(伪代码逻辑):

for e in (SELECT * FROM emp WHERE emp_id > 1):
    for s in (SELECT * FROM salary WHERE emp_id = e.emp_id):
        输出 e 和 s 的匹配行

这种嵌套循环结构就是 N-LJ,它是 MySQL 的默认关联方式,简单但当驱动表数据量大时,性能会迅速下滑


二、为什么要特别关注“驱动表”?

在多表关联中,驱动表(即外层表)是最关键的,它决定了嵌套循环的“循环次数”。MySQL 查询优化器默认会选择一个驱动表,但其选择是否合适,取决于索引与统计信息。

如果驱动表过大而无合适索引,将导致每一条数据都要进行嵌套查询,最终变成灾难性的性能瓶颈。


三、执行计划中的驱动表选择分析(实战案例)

3.1 场景模拟

我们用如下三张表进行演示:

  • chapter:图书章节表(chapter_id, book_id, chapter_name)
  • member:会员表(member_id, is_vip)
  • history:会员阅读记录表(history_id, member_id, chapter_id, read_time)

目标查询:

查询编号为 103 的图书中,所有 VIP 用户的阅读历史记录。

SQL 示例:

SELECT *
FROM history h
JOIN chapter c ON h.chapter_id = c.chapter_id
JOIN member m ON h.member_id = m.member_id
WHERE c.book_id = 103 AND m.is_vip = 1;

3.2 未加索引情况下的执行计划分析

  • 执行计划中显示 h 表(history)为驱动表,MySQL 扫描了 57 万条记录;
  • 然后依次 join 到 c 表和 m 表;
  • 由于 chapter_idmember_id 没有索引,所以是全表扫描。

问题点:

  • 驱动表数据量过大,嵌套查询次数惊人;
  • 缺少索引,导致全表扫描 + 低效关联;
  • 最终执行性能极差。

四、如何正确优化多表关联查询?

4.1 主外键字段必须建立索引!

很多初学者以为“只对 WHERE 条件字段建索引”就够了,其实不然。多表关联优化的核心在于:主键字段自动带索引,但外键字段(如 chapter_id, member_id)必须手动加索引。

正确方式:

  • history.chapter_id 加索引;
  • history.member_id 加索引;
  • 对于过滤字段如 book_id, is_vip 也应加索引。
ALTER TABLE history ADD INDEX idx_chapter_id (chapter_id);
ALTER TABLE history ADD INDEX idx_member_id (member_id);
ALTER TABLE chapter ADD INDEX idx_book_id (book_id);
ALTER TABLE member ADD INDEX idx_is_vip (is_vip);

4.2 索引生效后的执行变化

  • MySQL 查询优化器会重新评估驱动表;
  • 驱动表可能由 history 变为 chapter
  • 扫描范围从数十万行变为几十行,执行效率大幅提升。

五、子查询与关联查询执行计划比较

很多开发者喜欢用 IN 子查询替代多表关联,比如:

SELECT * FROM history 
WHERE member_id IN (
    SELECT member_id FROM member WHERE is_vip = 1
);

在 MySQL 8.0 中,如果建立了正确索引,优化器会自动把 IN 子查询转换为半连接(Semi Join),也就是类似 N-LJ 的执行方式。

只要索引对,子查询与 JOIN 在执行计划上并不会差太多。


六、字段中的依赖子查询(Dependent SubQuery)

再看一个特殊场景:字段级别的子查询

SELECT h.*, 
    (SELECT chapter_name FROM chapter c WHERE c.chapter_id = h.chapter_id) AS chapter_name 
FROM history h;

这种查询在每一行 history 的结果上都会再次查询 chapter 表一次,我们称为 Dependent SubQuery,也属于 N-LJ 的一种,但效率远低于 JOIN。

建议避免在 SELECT 子句中写依赖子查询,改写成 JOIN 结构更高效。


七、带 UNION 的子查询执行计划

考虑如下子查询结构:

SELECT * FROM history 
WHERE chapter_id IN (
    SELECT chapter_id FROM chapter WHERE book_id = 103
    UNION
    SELECT chapter_id FROM chapter WHERE book_id = 104
);

执行计划分析:

  • 第一行出现 drid 3,代表驱动表来自执行计划 ID 为 3 和 4 的子查询;
  • UNION 结果形成中间临时表;
  • 再驱动外部查询。

如果是 UNION ALL,则只是连接结果集,不做去重,性能更好。而使用 UNION(默认去重),会触发:

使用临时表 + filesort(排序),如果超出内存限制,还会落盘(变成 MyISAM 表),造成严重性能问题。

建议:

  • 若数据可重复,优先使用 UNION ALL 替代 UNION
  • 尽量控制子查询结果集数量;
  • 增大临时表内存配置 tmp_table_sizemax_heap_table_size
  • 使用 LIMIT 限制子查询数据量。

八、总结:多表关联优化原则

优化点 说明
主外键字段建索引 主表字段通常自动有索引,外键字段需要手动添加
控制驱动表的数据量 数据量越小,嵌套查询越高效
尽量避免 SELECT 子句中用子查询 可读性差,性能低
用 JOIN 替代 IN、EXISTS、字段级子查询 执行效率更高,计划更清晰
谨慎使用 UNION 如果非必要去重,尽量使用 UNION ALL
多表关联尽量控制在 3 张以内 避免执行计划复杂化与优化器判断错误

结语

多表关联查询在日常业务中非常常见,但其执行效率常常决定系统响应速度。理解其底层执行机制、优化器策略与执行计划的各字段含义,是成为 SQL 优化高手的必经之路。

你可能感兴趣的:(MySQL 多表关联执行计划全面解析:从 N-LJ 到子查询优化)