CREATE TABLE `t1` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`), KEY `a` (`a`)
) ENGINE=InnoDB;
t1里插入100条数据
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`), KEY `a` (`a`)
) ENGINE=InnoDB;
t2里插入1000调数据
Index Nested-Loop Join
//straight_join让MySQL使用固定的连接方式进行查询
select * from t1 straight_join t2 on (t1.a=t2.a);
t1是驱动表,t2是被驱动表
- 从表 t1 中读入一行数据 R;
- 从数据行 R 中,取出 a 字段到表 t2 里去查找;
- 取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;
- 重复执行上述步骤,直到表 t1 的末尾循环结束。
这种查询操作可以用上被驱动表t2上的索引,叫做 Index Nested-Loop Join。
对应流程如下:
复杂度: 驱动表t1做了全表扫描,共100行,被驱动表通过树搜索,由于构造的数据一一对应,所以也为扫描100行,共计扫描200行。
然后分析下如果只用单表查询,发现是查处表t1的所有数据,遍历所有数据,每一行执行查询SQL,这样看来多增加了很多次的交互,这样看来还不如Join的好
所以本质上还是可以使用Join的。
在join执行中,驱动表走全表扫描,被驱动表走树搜索。
那么假设驱动表的行数为N,被驱动表的行数为M,被驱动表搜索一行数据的复杂度为2∗log2M 2*log_2M 2∗log2M 驱动表的行数是N,所以整体复杂度为
N+N∗2∗log2M N+N*2*log_2M N+N∗2∗log2M整体来看,N对整体扫描行数的影响更大。
综上:前提是可以使用被驱动表的索引
- 使用join语句比单表执行SQL性能要好
- 使用小表做驱动表
Simple Nested-Loop Join
由于表 t2 的字段 b 上没有索引,每次到 t2 去匹配的时候,就要做一次全表扫描。
select * from t1 straight_join t2 on (t1.a=t2.b);
按照之前的数据量计算,需要扫描100*1000=10万行
MySQL最终没有采用这个方案,而采用了另外一种方案
Block Nested-Loop Join
当被驱动表上没有索引,流程如下:
- 把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;
- 扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。
在这个过程中,对表 t1 和 t2 都做了一次全表扫描,因此总的扫描行数是 1100。由于 join_buffer 是以无序数组的方式组织的,因此对表 t2 中的每一行,都要做 100 次判断,总共需要在内存中做的判断次数是:100*1000=10 万次。因为是在内存里操作,所以比Simple Nested-Loop Join这种方案好
总体复杂度为
- 两个表都做一次全表扫描,所以总的扫描行数是 M+N;
- 内存中的判断次数是 M*N。
当join_buffer放不下的时候,就采用分段放的方式。
- 扫描表 t1,顺序读取数据行放入 join_buffer 中,放完第 88 行 join_buffer 满了,继续第 2 步;
- 扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回;
- 清空 join_buffer;
- 继续扫描表 t1,顺序读取最后的 12 行数据放入 join_buffer 中,继续执行第 2 步。
这个流程才体现出了这个算法名字中“Block”的由来,表示“分块去 join”。
假设,驱动表的数据行数是 N,需要分 K 段才能完成算法流程,被驱动表的数据行数是 M。
注意,这里的 K 不是常数,N 越大 K 就会越大,因此把 K 表示为λ*N,显然λ的取值范围是 (0,1)。
所以,在这个算法的执行过程中:
- 扫描行数是 N+λNM;
- 内存判断 N*M 次。
显然,内存判断次数是不受选择哪个表作为驱动表影响的。而考虑到扫描行数,在 M 和 N 大小确定的情况下,N 小一些,整个算式的结果会更小。
所以让小表当驱动表。
在 N+λNM 这个式子里,λ才是影响扫描行数的关键因素,这个值越小越好。
什么参数会影响 K 的大小呢?(也就是λ的大小)答案是 join_buffer_size。join_buffer_size 越大,一次可以放入的行越多,分成的段数也就越少,对被驱动表的全表扫描次数就越少。
这就是为什么,你可能会看到一些建议告诉你,如果你的 join 语句很慢,就把 join_buffer_size 改大。
常见问题
能不能使用 join 语句?
- 如果可以使用 Index Nested-Loop Join 算法,也就是说可以用上被驱动表上的索引,其实是没问题的;
- 如果使用 Block Nested-Loop Join 算法,扫描行数就会过多。尤其是在大表上的 join 操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用。
如果要使用 join,应该选择大表做驱动表还是选择小表做驱动表?
- 如果是 Index Nested-Loop Join 算法,应该选择小表做驱动表;
- 如果是 Block Nested-Loop Join 算法:
- 在 join_buffer_size 足够大的时候,是一样的;
- 在 join_buffer_size 不够大的时候(这种情况更常见),应该选择小表做驱动表。
总是使用小表做驱动表
小表的定义
决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。