mysql--join

Join

两个表都有一个主键索引 id 和一个索引 a,字段 b 上无索引,表 t2 里插入了 1000 行数据,在表 t1 里插入的是 100 行数据

sql 复制代码
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;
 
drop procedure idata;
delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=1000)do
    insert into t2 values(i, i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();
 
create table t1 like t2;
insert into t1 (select * from t2 where id<=100)

straight_join 让 MySQL 使用固定的连接方式执行查询,前表驱动后表

Index Nested-Loop Join

前提是"可以使用被驱动表的索引"

mysql 复制代码
-- t1 是驱动表,t2 是被驱动表
-- 被驱动表 t2 的字段 a 上有索引,join 过程用上了这个索引
select * from t1 straight_join t2 on (t1.a=t2.a);

执行流程:

1、从表 t1 中读入一行数据 R;

2、从数据行 R 中,取出 a 字段到表 t2 里去查找;

3、取出表 t2 中满足条件的行,跟 R 组成一行,作为结果集的一部分;

4、重复执行步骤 1 到 3,直到表 t1 的末尾循环结束。

先遍历表 t1,然后根据从表 t1 中取出的每行数据中的 a 值,去表 t2 中查找满足条件的记录。与嵌套查询类似,并且可以用上被驱动表的索引 ,所以我们称之为"Index Nested-Loop Join",简称 NLJ。

分析:

对驱动表 t1 做了全表扫描,这个过程需要扫描 100 行;

而对于每一行 R,根据 a 字段去表 t2 查找,走的是树搜索过程。由于我们构造的数据都是一一对应的,因此每次的搜索过程都只扫描一行,也是总共扫描 100 行;

所以,整个执行流程,总扫描行数是 200。

复杂度:

这个 join 语句执行过程中,驱动表是走全表扫描,而被驱动表是走树搜索。

假设被驱动表的行数是 M。每次在被驱动表查一行数据,要先搜索索引 a,再搜索主键索引。每次搜索一棵树近似复杂度是以 2 为底的 M 的对数,记为 log2M,所以在被驱动表上查一行的时间复杂度是 2log2M。
假设驱动表的行数是 N,执行过程就要扫描驱动表 N 行,然后对于每一行,到被驱动表上匹配一次。
因此整个执行过程,近似复杂度是 N + N
2*log2M。N 对扫描行数的影响更大,因此应该让小表来做驱动表。

结论:

使用 join 语句,性能比强行拆成多个单表执行 SQL 语句的性能要好;

如果使用 join 语句的话,需要让小表做驱动表

Block Nested-Loop Join

被驱动表上没有可用的索引

mysql 复制代码
-- 由于表 t2 的字段 b 上没有索引,每次到 t2 去匹配的时候,就要做一次全表扫描。
select * from t1 straight_join t2 on (t1.a=t2.b);

执行流程:

1、把表 t1 的数据读入线程内存 join_buffer 中,由于我们这个语句中写的是 select *,因此是把整个表 t1 放入了内存;

2、扫描表 t2,把表 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回。

复杂度:

假设小表的行数是 N,大表的行数是 M

两个表都做一次全表扫描,所以总的扫描行数是 M+N;

内存中的判断次数是 M*N。

调换这两个算式中的 M 和 N 没差别,因此这时候选择大表还是小表做驱动表,执行耗时是一样的。

join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。

  • 如果 join_buffer 放不下表 t1 的所有数据话,策略很简单,就是分段放

执行过程:

1、扫描表 t1,顺序读取数据行放入 join_buffer 中,放完第 88 行 join_buffer 满了,继续第 2 步;

2、扫描表 t2,把 t2 中的每一行取出来,跟 join_buffer 中的数据做对比,满足 join 条件的,作为结果集的一部分返回;

3、清空 join_buffer;

4、继续扫描表 t1,顺序读取最后的 12 行数据放入 join_buffer 中,继续执行第 2 步。

分析:

假设,驱动表的数据行数是 N,需要分 K 段才能完成算法流程,被驱动表的数据行数是 M。

K 不是常数,N 越大 K 就会越大,因此把 K 表示为λ*N,显然λ的取值范围是 (0,1)。

复杂度:

扫描行数是 N+λN M;

内存判断 N*M 次。

内存判断次数是不受选择哪个表作为驱动表影响的。而考虑到扫描行数,在 M 和 N 大小确定的情况下,N 小一些,整个算式的结果会更小。

使用 join 语句

1、如果可以使用 Index Nested-Loop Join 算法,也就是说可以用上被驱动表上的索引,其实是没问题的;

2、如果使用 Block Nested-Loop Join 算法,扫描行数就会过多。尤其是在大表上的 join 操作,这样可能要扫描被驱动表很多次,会占用大量的系统资源。所以这种 join 尽量不要用。

join 使用"小表"做驱动表

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是"小表",应该作为驱动表。

join优化

Multi-Range Read 优化 (MRR)

优化的主要目的是尽量使用顺序读盘

回表是指,InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键索引上去查整行数据的过程。

主键索引是一棵 B+ 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。回表是一行行搜索主键索引的

如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,那么就会出现随机访问,性能相对较差。
按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能

MRR 优化的设计思路:

1、根据索引 a,定位到满足条件的记录,将 id 值放入 read_rnd_buffer 中 ;

2、将 read_rnd_buffer 中的 id 进行递增排序;

3、排序后的 id 数组,依次到主键 id 索引中查记录,并作为结果返回。

read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制的。如果步骤 1 中,read_rnd_buffer 放满了,就会先执行完步骤 2 和 3,然后清空 read_rnd_buffer。之后继续找索引 a 的下个记录,并继续循环。

想要稳定地使用 MRR 优化的话,需要设置set optimizer_switch="mrr_cost_based=off"。(官方文档的说法,是现在的优化器策略,判断消耗的时候,会更倾向于不使用 MRR,把 mrr_cost_based 设置为 off,就是固定使用 MRR 了。)

Batched Key Access

BKA 算法,其实就是对 NLJ 算法的优化

NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了。

  • 要使用 BKA 优化算法的话,需要在执行 SQL 语句之前,先设置
mysql 复制代码
-- 前两个参数的作用是要启用 MRR。BKA 算法的优化要依赖于 MRR。
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

BNL 算法的性能问题

Block Nested-Loop Join(BNL) 算法对系统的影响

1、可能会多次扫描被驱动表,占用磁盘 IO 资源;

2、判断 join 条件需要执行 M*N 次对比(M、N 分别是两张表的行数),如果是大表就会占用非常多的 CPU 资源;

3、可能会导致 Buffer Pool 的热数据被淘汰,影响内存命中率。

  • 影响 Buffer Pool

由于 InnoDB 对 Bufffer Pool 的 LRU 算法做了优化,即:第一次从磁盘读入内存的数据页,会先放在 old 区域。如果 1 秒之后这个数据页不再被访问了,就不会被移动到 LRU 链表头部,这样对 Buffer Pool 的命中率影响就不大。

如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到 LRU 链表头部。

这种情况对应的,是冷表的数据量小于整个 Buffer Pool 的 3/8,能够完全放入 old 区域的情况。

如果这个冷表很大,就会出现另外一种情况:业务正常访问的数据页,没有机会进入 young 区域。

由于优化机制的存在,一个正常访问的数据页,要进入 young 区域,需要隔 1 秒后再次被访问到。但是,由于 join 语句在循环读磁盘和淘汰内存页,进入 old 区域的数据页,很可能在 1 秒之内就被淘汰了。这样,就会导致这个 MySQL 实例的 Buffer Pool 在这段时间内,young 区域的数据页没有被合理地淘汰。

也就是说,这两种情况都会影响 Buffer Pool 的正常运作。

BNL 优化

1、BNL 转 BKA

1、一些情况下,我们可以直接在被驱动表上建索引,这时就可以直接转成 BKA 算法了

2、不适合在被驱动表上建索引的情况

sql 复制代码
select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000;

在表 t2 中插入了 100 万行数据,但是经过 where 条件过滤后,需要参与 join 的只有 2000 行数据。如果这条语句同时是一个低频的 SQL 语句,那么再为这个语句在表 t2 的字段 b 上创建一个索引就很浪费了。

  • 这时候,我们考虑使用临时表
sql 复制代码
  -- 把表 t2 中满足条件的数据放在临时表 tmp_t 中;
  -- 为了让 join 使用 BKA 算法,给临时表 tmp_t 的字段 b 加上索引;
   create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
  insert into temp_t select * from t2 where b>=1 and b<=2000;
  -- 让表 t1 和 tmp_t 做 join 操作。
  select * from t1 join temp_t on (t1.b=temp_t.b);
  • 业务端自己实现在

1、select * from t1;取得表 t1 的全部 1000 行数据,在业务端存入一个 hash 结构

2、select * from t2 where b>=1 and b<=2000; 获取表 t2 中满足条件的 2000 行数据。

3、把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。

入一个 hash 结构

2、select * from t2 where b>=1 and b<=2000; 获取表 t2 中满足条件的 2000 行数据。

3、把这 2000 行数据,一行一行地取到业务端,到 hash 结构的数据表中寻找匹配的数据。满足匹配的条件的这行数据,就作为结果集的一行。

相关推荐
打鱼又晒网25 分钟前
【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
数据库·mysql
大白要努力!30 分钟前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
tatasix1 小时前
MySQL UPDATE语句执行链路解析
数据库·mysql
南城花随雪。1 小时前
硬盘(HDD)与固态硬盘(SSD)详细解读
数据库
儿时可乖了2 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
懒是一种态度2 小时前
Golang 调用 mongodb 的函数
数据库·mongodb·golang
天海华兮2 小时前
mysql 去重 补全 取出重复 变量 函数 和存储过程
数据库·mysql
gma9992 小时前
Etcd 框架
数据库·etcd
爱吃青椒不爱吃西红柿‍️3 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
Yz98763 小时前
hive的存储格式
大数据·数据库·数据仓库·hive·hadoop·数据库开发