在实际企业开发中,多表关联查询更为常见,也是导致 SQL 执行效率低下的重要原因之一。
今天,我们将系统性地解析 MySQL 多表关联查询的执行机制,重点包括:
在 MySQL 中,多表连接最常见的执行策略就是 Nested Loop Join(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 查询优化器默认会选择一个驱动表,但其选择是否合适,取决于索引与统计信息。
如果驱动表过大而无合适索引,将导致每一条数据都要进行嵌套查询,最终变成灾难性的性能瓶颈。
我们用如下三张表进行演示:
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;
h
表(history)为驱动表,MySQL 扫描了 57 万条记录;c
表和 m
表;chapter_id
和 member_id
没有索引,所以是全表扫描。问题点:
很多初学者以为“只对 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);
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 在执行计划上并不会差太多。
再看一个特殊场景:字段级别的子查询:
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 结构更高效。
考虑如下子查询结构:
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 ALL
,则只是连接结果集,不做去重,性能更好。而使用 UNION
(默认去重),会触发:
使用临时表 + filesort(排序),如果超出内存限制,还会落盘(变成 MyISAM 表),造成严重性能问题。
建议:
UNION ALL
替代 UNION
;tmp_table_size
、max_heap_table_size
;LIMIT
限制子查询数据量。优化点 | 说明 |
---|---|
主外键字段建索引 | 主表字段通常自动有索引,外键字段需要手动添加 |
控制驱动表的数据量 | 数据量越小,嵌套查询越高效 |
尽量避免 SELECT 子句中用子查询 | 可读性差,性能低 |
用 JOIN 替代 IN、EXISTS、字段级子查询 | 执行效率更高,计划更清晰 |
谨慎使用 UNION | 如果非必要去重,尽量使用 UNION ALL |
多表关联尽量控制在 3 张以内 | 避免执行计划复杂化与优化器判断错误 |
多表关联查询在日常业务中非常常见,但其执行效率常常决定系统响应速度。理解其底层执行机制、优化器策略与执行计划的各字段含义,是成为 SQL 优化高手的必经之路。