mysql - 第7课 - join关联查询优化详解

注:当前测试mysql版本:mysql5.7,编码utf8mb4

准备表和数据的脚本:

-- 学生表
DROP TABLE IF EXISTS `t_student`;
CREATE TABLE `t_student` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `std_name` varchar(30) NOT NULL,
  `age` tinyint(3) unsigned NOT NULL,
  `class_id` int(11) unsigned NOT NULL,
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_std_name_age_class` (`std_name`,`age`,`class_id`) USING BTREE,
  KEY `idx_std_class` (`class_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

--添加测试数据的存储过程
drop procedure if exists proc_insert_student; 

DELIMITER ;;
create procedure proc_insert_student()        
begin
  declare i int;                    
  set i=1;                     
  while(i<=10000)do                 
    insert into t_student(std_name,age,class_id) values(CONCAT((SUBSTRING_INDEX(SUBSTRING_INDEX('Li Lei,Han Mei,Poli',',',(i%3+1)),',',-1)),i), (i%120)+1 ,(i%10)+1);   
    set i=i+1;                       
  end while;
end;;
DELIMITER ;

-- 执行存储过程
call proc_insert_student();


-- 班级表
 DROP TABLE IF EXISTS `t_class`;
 CREATE TABLE `t_class` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `class_name` varchar(30) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
insert into t_class(class_name) values('class 1');
insert into t_class(class_name) values('class 2');
insert into t_class(class_name) values('class 3');
insert into t_class(class_name) values('class 4');
insert into t_class(class_name) values('class 5');
insert into t_class(class_name) values('class 6');
insert into t_class(class_name) values('class 7');
insert into t_class(class_name) values('class 8');
insert into t_class(class_name) values('class 9');
insert into t_class(class_name) values('class 10');

驱动表和被驱动表

三种关联方式:

 -- sql1 right join:
 explain select * from t_student t1 right join t_class t2 on t1.class_id=t2.id;
 -- sql2 left join:
 explain select * from t_student t1 left join t_class t2 on t1.class_id=t2.id;
 -- sql3 inner join:
 explain select * from t_student t1 inner join t_class t2 on t1.class_id=t2.id;
 -- sql3 inner join 更换表顺序:
 explain select * from t_class t2 inner join t_student t1 on t1.class_id=t2.id;

执行结果(关联字段都有索引):

image.png

从explain的查询顺序看到:right join时右边表先查询;left join时左边表先查询;inner join时跟前后顺序无关,在关联字段都建了索引的情况下,mysql会自动选一张数据量较小的表先查询。(如果t1表不建class_id的索引会先查询t1表,可以自行测试)。

先查询的叫驱动表,后查询的叫被驱动表

注:执行顺序是根据explain的id列,id越大越先执行,id相等时执行前面的。第二节课有讲解。

两种循环连接算法

一. Nested-Loop Join(NLJ)嵌套循环连接算法

NLJ定义:从驱动表里循环读取每行数据,在这行数据中取到关联字段,根据关联字段被驱动表里匹配到满足条件的行,然后取出两张表的结果合集。

例:explain select * from t_student t1 inner join t_class t2 on t1.class_id=t2.id;

image.png

从以上explain可知 :

1.t2 是驱动表先查询,t1 是被驱动表后查询。

2.使用了 NLJ 算法。一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer 则表示使用的 join 算法是 NLJ

查询流程大致如下:

1.先从 t2 取出一行数据(如果 t2 表有查询过滤条件的,会从过滤结果里取出一行数据);

2.从第 1 步的数据中,取出关联字段 id 的值,到表 t1 中与 class_id 匹配;

3.取出 t1 中满足条件的行,跟 t2 中获取到的结果合并,作为结果返回给客户端;

4.循环上面三步直至 t2 数据用完。

伪代码逻辑大致如下:

for (select t2.* from t_class t2) {  // 第一步,从磁盘循环扫描 t_class,一共扫描10行
    select t1.* from t_student t1 where t1.class_id=t2.id;    //第二步,拿t2的id走t1表的idx_std_class索引每次匹配到995行数据
    t1.* + t2.*;  //第三步合并两表结果
}

扫描行数计算:

整个过程中读取了t2的全表数据10行,遍历每行数据取到id字段的值, 拿着id值在t1表的索引 idx_std_class 中进行扫描,每次扫描到 995 行数据。整个过程扫描了10+995*10=9960行。

现在我删除 t_student 表的 idx_std_class 索引再测试一次。

-- 删除索引脚本:

alter table t_student drop index idx_std_class;

image.png

从以上explain看到,删除索引后,数据量较大的 t_student 表成了驱动表,数据量小的 t_class 成了被驱动表。

伪代码逻辑大致如下:

for (select t1.* from t_student t1) {  //第一步,从磁盘循环扫描t_student,一共扫描9958行
    select t2.* from t_class t2 where t2.id=t1.class_id;  //第二步,走t2的主键索引,每次扫描到1行数据
    t1.* + t2.*;  //第三步合并两表结果
}

t1 首先扫描到 9958 行数据,然后循环这些数据,取 class_id 的值在 t2 表中进行匹配,每次走索引匹配到1行数据,一共9958次。整个过程扫描了 9958 + 9958 = 19916 行,比上面的 9960行 性能低了一倍。所以在优化关联查询的时候需要把大表作为被驱动表,在大表的关联字段上建立索引。

结论:只有一个索引的时候,使用没索引的表作为驱动表。

二. Block Nested-Loop Join(BNL)基于块的嵌套循环连接算法

如果两个关联的字段都不建索引会怎么样?

我给 t_class 表再建一列 id2,不建立索引,并且值跟 id 一样。

alter table t_class add column id2 int(11) unsigned NOT NULL;

update t_class set id2=id;

image.png

执行sql,注意用t_class的id2关联

explain select * from t_student t1 inner join t_class t2 on t1.class_id=t2.id2;

image.png

可以看到extra列中有关键字 Using join buffer (Block Nested Loop),

这意味着当关联字段都没有索引的时候会用 BNL 连接算法

上面的sql查询流程大致如下:

1.把驱动表 t2 的所有数据放入 join_buffer 中。

2. 把表 t1 中每一行取出来,跟 join_buffer 中的数据做对比。

3. 返回满足 join 条件的数据。

join_buffer 是mysql在内存中分配的一块内存,用于BNL算法时存储驱动表数据使用。默认值256k,可由参数 join_buffer_size 设定

伪代码逻辑大致如下:

List bufferList = select t2.* from t_class t2 ; //第一步,先从磁盘全表扫描t_class 10行数据,放入join_buffer 内存中
for (select t1.* from t_student t1) {   //第二步,从磁盘循环扫描 t_student,一共扫描9958行
    for (int t2RowData : bufferList ) {  //第三步:从join_buffer 内存中循环拿 t2 表的 id 与 t1 表的 class_id匹配,返回满足条件的数据
        if (t2RowData.id = t1.class_id) {
            t2RowData+t1.*;
        }
    }
}

扫描行数及内存判断次数:

从type列可以看到两个表都做了一次全表扫描,扫描磁盘总行数为 9958 + 10 = 9968行。另外join buffer 中的 t2 表的数据是无序的,每次扫描都要做10次判断,在内存中一共要判断10*9958=99580次。

使用BNL算法一共扫描磁盘9968行,如果使用NLJ算法会怎样?

因为没有索引,t2 表每次取1行数据与 t1 表进行匹配时都需要进行全表扫描,一共扫描 10*9958=99580行。这个几乎是BNL算法的10倍了,而且是磁盘扫描。相比之下BNL基于内存的算法会快得多。

join_buffer 如果一次存不下驱动表里的所有数据会怎么办?

如果 join_buffer 一次存不下驱动表里的所有数据就会进行分段放,也就是把驱动表里的数据分成多个片段。

再拿上面的t假设 join_buffer 只能存1000行数据,而例子中的驱动表 t2 表里有1500行,那就先往 join_buffer 中放入1000行数据,然后从 t1 表中取数据跟 join_buffer 中的数据进行对比得到结果,然后清空join_buffer ,再把剩下的500行放入join_buffer ,然后再从 t1 表里取数据与join_buffer 中的数据进行对比得到结果。

straight_join关键字

前面说到我们可以使用left join控制左表为驱动表,right join控制右表为驱动表。但是inner jion的时候只能由mysql自己判断驱动表。

straight 可以控制左表作为驱动表,同时保证inner join的连接效果。例:

explain select * from t_student t1 straight_join t_class t2 on t1.class_id=t2.id;

explain select * from t_class t2 straight_join t_student t1 on t1.class_id=t2.id;

(关联字段都有索引)

image.png

建议不要轻易使用straight_join关键字,mysql会使用优化器自动分析。利用好关联查询的规则即可。

总结:

关联查询算法规则:

1.left join时左表作为驱动表。right join时右边作为驱动表。inner join时mysql会根据索引和数据量判断,关联字段都有索引时选择数据量较小的作为驱动表(小表驱动大表),只有一个索引时选择无索引表作为驱动表(无索引驱动有索引)。

2.连接字段有索引的时候使用 NLJ 算法。

3.两个连接字段都有索引的时候,选择小表作为驱动表。

3.两个连接字段其中一个有索引的时候,选择无索引的表作为驱动表(无视大表小表)。

4.两个连接字段都没索引的时候使用 BNL 算法。

关联查询优化建议:

1.使用小表作为驱动表,小表驱动大表

2.给关联字段加索引,让mysql做join操作时尽量选择 NLJ 算法。

3.尽量给两个关联字段都加索引,这时候mysql会选择小表作为驱动表,同时在进行循环匹配时大表(被驱动表)的索引会提高匹配效率。(如果只给大表加索引,mysql会选择大表作为驱动表)。

猜你喜欢

转载自blog.csdn.net/qq_33743572/article/details/107762676
今日推荐