MySQL Join 实现原理分析

本文详细解析了 MySQL 中的 Nested Loop Join 算法及其在具体查询中的执行过程,包括如何通过索引优化提升性能,并通过示例展示了优化前后执行计划的变化。
在MySQL 中,只有一种 Join 算法,就是大名鼎鼎的 Nested Loop Join,他没有其他很多数据库所提供的 Hash Join,也没有 Sort Merge Join。顾名思义,Nested Loop Join 实际上就是通过驱动表的结果集作为循环基础数据,然后一条一条的通过该结果集中的数据作为过滤条件到下一个表中查询数据,然后合并结果。如果还有第三个参与 Join,则再通过前两个表的 Join 结果集作为循环基础数据,再一次通过循环查询条件到第三个表中查询数据,如此往复。

  还是通过示例和图解来说明吧,后面将通过我个人数据库测试环境中的一个 example(自行设计,非MySQL 自己提供) 数据库中的三个表的 Join 查询来进行示例。

  注意:由于这里有些内容需要在MySQL 5.1.18之后的版本中才会体现出来,所以本测试的MySQL 版本为5.1.26

  表结构:

1 sky@localhost : example 11:09:32> show create table user_group\G
2
3   *************************** 1. row ***************************
4
5   Table: user_group
6
7   Create Table: CREATE TABLE `user_group` (
8
9   `user_id` int(11) NOT NULL,
10
11   `group_id` int(11) NOT NULL,
12
13   `user_type` int(11) NOT NULL,
14
15   `gmt_create` datetime NOT NULL,
16
17   `gmt_modified` datetime NOT NULL,
18
19   `status` varchar(16) NOT NULL,
20
21   KEY `idx_user_group_uid` (`user_id`)
22
23   ) ENGINE=MyISAM DEFAULT CHARSET=utf8
24
25   1 row in set (0.00 sec)
26
27   sky@localhost : example 11:10:32> show create table group_message\G
28
29   *************************** 1. row ***************************
30
31   Table: group_message
32
33   Create Table: CREATE TABLE `group_message` (
34
35   `id` int(11) NOT NULL AUTO_INCREMENT,
36
37   `gmt_create` datetime NOT NULL,
38
39   `gmt_modified` datetime NOT NULL,
40
41   `group_id` int(11) NOT NULL,
42
43   `user_id` int(11) NOT NULL,
44
45   `author` varchar(32) NOT NULL,
46
47   `subject` varchar(128) NOT NULL,
48
49   PRIMARY KEY (`id`),
50
51   KEY `idx_group_message_author_subject` (`author`,`subject`(16)),
52
53   KEY `idx_group_message_author` (`author`),
54
55   KEY `idx_group_message_gid_uid` (`group_id`,`user_id`)
56
57   ) ENGINE=MyISAM AUTO_INCREMENT=97 DEFAULT CHARSET=utf8
58
59   1 row in set (0.00 sec)
60
61   sky@localhost : example 11:10:43> show create table group_message_content\G
62
63   *************************** 1. row ***************************
64
65   Table: group_message_content
66
67   Create Table: CREATE TABLE `group_message_content` (
68
69   `group_msg_id` int(11) NOT NULL,
70
71   `content` text NOT NULL,
72
73   KEY `group_message_content_msg_id` (`group_msg_id`)
74
75   ) ENGINE=MyISAM DEFAULT CHARSET=utf8
76
77   1 row in set (0.00 sec)

使用Query如下:

1   select m.subject msg_subject, c.content msg_content
2
3   from user_group g,group_message m,group_message_content c
4
5   where g.user_id = 1
6
7   and m.group_id = g.group_id
8
9   and c.group_msg_id = m.id

看看我们的 Query 的执行计划:
1   sky@localhost : example 11:17:04> explain select m.subject msg_subject, c.content msg_content
2
3   -> from user_group g,group_message m,group_message_content c
4
5   -> where g.user_id = 1
6
7   -> and m.group_id = g.group_id
8
9   -> and c.group_msg_id = m.id\G
10
11   *************************** 1. row ***************************
12
13   id: 1
14
15   select_type: SIMPLE
16
17   table: g
18
19   type: ref
20
21   possible_keys: user_group_gid_ind,user_group_uid_ind,user_group_gid_uid_ind
22
23   key: user_group_uid_ind
24
25   key_len: 4
26
27   ref: const
28
29   rows: 2
30
31   Extra:
32
33   *************************** 2. row ***************************
34
35   id: 1
36
37   select_type: SIMPLE
38
39   table: m
40
41   type: ref
42
43   possible_keys: PRIMARY,idx_group_message_gid_uid
44
45   key: idx_group_message_gid_uid
46
47   key_len: 4
48
49   ref: example.g.group_id
50
51   rows: 3
52
53   Extra:
54
55   *************************** 3. row ***************************
56
57   id: 1
58
59   select_type: SIMPLE
60
61   table: c
62
63   type: ref
64
65   possible_keys: idx_group_message_content_msg_id
66
67   key: idx_group_message_content_msg_id
68
69   key_len: 4
70
71   ref: example.m.id
72
73   rows: 2
74
75   Extra:   
我们可以看出,MySQL Query Optimizer 选择了 user_group 作为驱动表,首先利用我们传入的条件 user_id 通过 该表上面的索引 user_group_uid_ind 来进行 const 条件的索引 ref 查找,然后以 user_group 表中过滤出来的结果集的 group_id 字段作为查询条件,对 group_message 循环查询,然后再通过 user_group 和 group_message 两个表的结果集中的 group_message 的 id 作为条件 与 group_message_content 的 group_msg_id 比较进行循环查询,才得到最终的结果。没啥特别的,后一个引用前一个的结果集作为条件,实现过程可以通过下图表示:

下面的我们调整一下 group_message_content 去掉上面的 idx_group_message_content_msg_id 这个索引,然后再看看会是什么效果:

1   sky@localhost : example 11:25:36> drop index idx_group_message_content_msg_id on group_message_content;
2
3   Query OK, 96 rows affected (0.11 sec)
4
5   sky@localhost : example 10:21:06> explain
6
7   -> select m.subject msg_subject, c.content msg_content
8
9   -> from user_group g,group_message m,group_message_content c
10
11   -> where g.user_id = 1
12
13   -> and m.group_id = g.group_id
14
15   -> and c.group_msg_id = m.id\G
16
17   *************************** 1. row ***************************
18
19   id: 1
20
21   select_type: SIMPLE
22
23   table: g
24
25   type: ref
26
27   possible_keys: idx_user_group_uid
28
29   key: idx_user_group_uid
30
31   key_len: 4
32
33   ref: const
34
35   rows: 2
36
37   Extra:
38
39   *************************** 2. row ***************************
40
41   id: 1
42
43   select_type: SIMPLE
44
45   table: m
46
47   type: ref
48
49   possible_keys: PRIMARY,idx_group_message_gid_uid
50
51   key: idx_group_message_gid_uid
52
53   key_len: 4
54
55   ref: example.g.group_id
56
57   rows: 3
58
59   Extra:
60
61   *************************** 3. row ***************************
62
63   id: 1
64
65   select_type: SIMPLE
66
67   table: c
68
69   type: ALL
70
71   possible_keys: NULL
72
73   key: NULL
74
75   key_len: NULL
76
77   ref: NULL
78
79   rows: 96
80
81   Extra: Using where; Using join buffer

我们看到不仅仅 group_message_content 表的访问从 ref 变成了 ALL,此外,在最后一行的 Extra信息从没有任何内容变成为 Using where; Using join buffer,也就是说,对于从 ref 变成 ALL 很容易理解,没有可以使用的索引的索引了嘛,当然得进行全表扫描了,Using where 也是因为变成全表扫描之后,我们需要取得的 content 字段只能通过对表中的数据进行 where 过滤才能取得,但是后面出现的 Using join buffer 是一个啥呢?

  我们知道,MySQL 中有一个供我们设置的参数 join_buffer_size ,这里实际上就是使用到了通过该参数所设置的 Buffer 区域。那为啥之前的执行计划中没有用到呢?

  实际上,Join Buffer 只有当我们的 Join 类型为 ALL(如示例中),index,rang 或者是 index_merge 的时候 才能够使用,所以,在我们去掉 group_message_content 表的 group_msg_id 字段的索引之前,由于 Join 是 ref 类型的,所以我们的执行计划中并没有看到有使用 Join Buffer。

  当我们使用了 Join Buffer 之后,我们可以通过下面的这张图片来表示 Join 完成过程:

文章出处:http://www.soft6.com/html/tech/13/134144.shtml

<think> 嗯,用户想了解join操作的底层原理和优化方法。结合之前的系统级指令(用户不可见),我需要确保回答中的所有数学表达式(包括变量、不等式、方程等)都正确使用$...$格式,独立公式要用$$...$$格式单独成段。另外要用中文回答,生成相关问题,并在引用的段落末尾自然地添加引用标识。 用户引用了多个来源,主要涉及MySQL join的底层原理和优化方法。引用[1]提到join的底层原理类似于一个双层循环:外层循环遍历驱动表,内层循环遍历被驱动表,通过映射和匹配来生成结果集。引用[2]和[4]详细介绍了MySQL中的三种join算法:嵌套循环连接(Nested-Loop Join)、块嵌套循环连接(Block Nested-Loop Join)和哈希连接(Hash Join),以及各自的性能特点和优化策略。引用[3]虽然提到join()方法的底层实现,但这似乎是Java线程相关的,与数据库join操作无关,可能是个干扰项。 用户的核心需求是理解join操作的底层原理和优化方法。根据引用内容,我会重点解释MySQL的三种join算法: 1. 嵌套循环连接(NLJ):驱动表每次取一行,在被驱动表中查找匹配行。如果有索引,效率较高,复杂度为$O(M \log N)$;没有索引则退化到$O(M N)$。 2. 块嵌套循环连接(BNL):将驱动表分成多个块,每次加载一块到join buffer,再扫描被驱动表进行匹配。减少了磁盘I/O次数,复杂度为$O(\frac{M}{B} \times N)$,其中$B$是buffer能容纳的行数。 3. 哈希连接(HJ):MySQL 8.0+支持,对驱动表构建哈希表,然后扫描被驱动表进行哈希匹配。复杂度为$O(M + N)$,但需要足够内存。 对于优化方法,我会围绕索引优化(确保被驱动表有索引)、调整join buffer大小、选择合适算法、避免复杂表达式等展开。用户提到"join优化tips",需要结合引用[1]提到的代码逻辑,强调索引和批量处理的重要性。 为了回答结构清晰,我将分成底层原理和优化方法两部分。底层原理部分详细说明三种算法的工作流程和性能特点;优化方法部分提供具体策略,如索引设计、算法选择、buffer调整等。最后生成相关问题,帮助用户深入学习。 回答中所有数学表达式会严格按指令处理,例如驱动表行数$M$、被驱动表行数$N$等。引用标识会自然添加在相关段落末尾。 </think> 以下是MySQLJOIN操作的底层原理及优化方法的系统解析: --- ### **一、JOIN操作底层原理** #### 1. **嵌套循环连接(Nested-Loop Join, NLJ)** - **执行流程**: 设驱动表行数为$M$,被驱动表行数为$N$ ```plaintext for each row in 驱动表: 根据JOIN条件遍历被驱动表的所有行 若匹配则输出结果 ``` - **性能分析**: 时间复杂度:$O(M \times N)$ 若被驱动表的JOIN字段有索引 → 退化为 **Index Nested-Loop Join (INLJ)** 时间复杂度优化为:$O(M \log N)$ [^1][^2][^4] #### 2. **块嵌套循环连接(Block Nested-Loop Join, BNLJ)** - **适用场景**:被驱动表无可用索引时 - **核心优化**: 将驱动表数据分块加载到 **Join Buffer**(大小由`join_buffer_size`控制) 批量与被驱动表数据匹配,减少磁盘I/O次数 - **性能公式**: 时间复杂度:$O(\frac{M}{B} \times N)$ ($B$为Buffer容纳的行数)[^2][^4] #### 3. **哈希连接(Hash Join, HJ)** - **MySQL版本限制**:仅支持 **8.0+** - **执行流程**: 1. 在内存构建驱动表的**哈希表**(Key=JOIN字段) 2. 扫描被驱动表,计算哈希值匹配驱动表数据 - **优势**: 时间复杂度:$O(M + N)$,性能显著优于NLJ 需足够内存存放哈希表,否则触发磁盘临时表[^2][^4] --- ### **二、JOIN优化方法** #### 1. **索引优化** - **强制使用索引**: 确保被驱动表的JOIN字段有索引(如`ALTER TABLE t2 ADD INDEX idx_col(col)`) 避免全表扫描,触发INLJ算法[^1][^2][^4] - **覆盖索引**: 索引包含SELECT的所有字段(`EXPLAIN`中`Extra: Using index`) #### 2. **算法选择策略** - **MySQL 8.0+**:优先启用Hash Join ```sql SELECT /*+ HASH_JOIN(t1, t2) */ ... -- 强制使用HJ ``` - **低版本MySQL**: - 小表驱动大表(减少外层循环次数) - 无索引时扩大`join_buffer_size`(默认256KB → 建议4MB+)[^2][^4] #### 3. **改写查询逻辑** - **拆分复杂JOIN**: 将多表JOIN拆分为多个子查询,用临时表减少中间结果集 - **避免非等值JOIN**: `ON t1.col > t2.col`会导致BNLJ性能退化 #### 4. **参数调优** ```sql SET optimizer_switch = 'block_nested_loop=on'; -- 启用BNLJ SET join_buffer_size = 4194304; -- 设置4MB Join Buffer ``` #### 5. **执行计划分析** - 关键字段解读: - `type`:`ref`/`eq_ref`(索引有效),`ALL`(全表扫描警告) - `Extra`:`Using join buffer (Block Nested Loop)`(需优化)[^2][^4] --- ### **三、算法对比总结** | **算法** | **时间复杂度** | **适用场景** | 版本限制 | |----------------|------------------|----------------------------------|------------| | NLJ | $O(M \times N)$ | 被驱动表有索引 | 全版本 | | BNLJ | $O(\frac{M}{B} \times N)$ | 无索引,小表驱动大表 | 全版本 | | Hash Join | $O(M + N)$ | 等值JOIN,内存充足 | MySQL 8.0+ | > 注:通过`EXPLAIN FORMAT=JSON`可查看具体算法选择[^2][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值