文章内容
大体来说,MySQL 可以分为 Server 层和存储引擎层两部分。
- Server层:包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
- 存储引擎层:是一个可插拔的设计,也就是我们可以随意选择具体的存储引擎。server端通过API与存储引擎进行通信,这些接口屏蔽了不同存储引擎的差异。支持 InnoDB、MyISAM、Memory 等多个存储引擎。从 MySQL 5.5 版本开始, InnoDB 成为了 MySQL 的默认存储引擎。
下面,借助一张《MySQL实战45讲》的图,来解释整个执行流程:
一、连接器
第一步,你会先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:
mysql -h$ip -P$port -u$user -p
我们可以通过 TCP/IP ,命名管道和共享内存 ,Unix域套接字文件 等方式进行连接的建立。当连接到服务器时,服务器会对密码,用户名,进行验证。
- 如果用户名或密码不对,你就会收到一个 “Access denied for user” 的错误,然后客户端程序结束执行。
- 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
二、查询缓存
连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存。
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。
但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。
好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 query_cache_type 设置成 OFF,关闭查询缓存。也可以用 SQL_CACHE 显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from T whereID=10;
MySQL 8.0 版本直接将查询缓存的整块功能删掉了,也就是说 8.0 开始彻底没有这个功能了。
三、分析器
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。
- 分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
- MySQL 从你输入的 “select” 这个关键字识别出来,这是一个查询语句。它也要把字符串 “T” 识别成 “表名T”,把字符串 “ID” 识别成 “列ID”。
- 做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足 MySQL 语法。
- 如果你的语句不对,就会收到 “You have an error in your SQL syntax” 的错误提醒,比如下面这个语句 select 少打了开头的字母 “s”。
mysql> elect * from t where ID=1;
ERROR 1064 (42000): You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right
syntax touse near 'elect * from t where ID=1'at line 1
一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧挨着 “use near” 的内容。
四、优化器
经过了分析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的语句,这个语句是执行两个表的 join:
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
- 1、既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
- 2、也可以先从表 t2 里面取出 c=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。
五、执行器
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。
mysql> select * from T whereID=10;
ERROR 1142 (42000): SELECT command denied touser'b'@'localhost'fortable'T'
如果有权限,就打开表继续执行。打开表的时候,优化器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:
- 1、调用 InnoDB 引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
- 2、调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 3、执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
对于有索引的执行逻辑也差不多,第一次调用“取满足条件的第一行”这个接口 ,然后循环取“满足条件的下一行”这个接口 。这些接口存储引擎已经定义好了。
慢查询日志有个rows_examined的字段表示语句执行过程扫描了多少行,就是执行器每次调用引擎获取数据时累加的,但有时候,执行器调用一次,引擎扫描了多行,所以这个数和行数并不是完全关联的。
六、存储引擎
mysql的存储引擎是一个可插拔的设计,server层通过设定的一些api和存储引擎交互,存储引擎去实现这些api。常见的存储引擎有InnoDB,myISAM,memory。
1、InnoDB
1)特点
- 1. 64TB
- 2. 支持事务
- 3. 行级锁
- 4. 支持多版本并发控制机制(MVCC)
- 5. 支持聚簇索引
- 6. 支持数据缓存
- 7. 支持外键
2)InnoDB数据库文件
- tb_name.frm:表格式定义
- tb_name.ibd :数据文件
2、myISAM
1)缺点
- 1. 不支持事务
- 2. 最小粒度锁:表级
- 3. 读写相互阻塞,写入不能读,读时不能写
- 4. 不支持MVCC(支持多版本并发控制机制)
- 5. 不支持聚簇索引
- 6. 不支持数据缓存,只支持索引缓存
- 7. 不支持外键
- 8. 崩溃恢复性较差
2)优点
- 1. 最大支持256TB存储空间
- 2. 读取数据较快,占用资源较少
- 3. 直接记录了select count(0)的值
3)MyISAM引擎存储文件
- tbl_name.frm: 表格式定义
- tbl_name.MYD: 数据文件
- tbl_name.MYI: 索引文件
4)适用场景
适用场景:MySQL5.5.5前默认的数据库引擎,在只读(或者写较少)、表较小(可以接受长时间进行修复操作)的场景适用
3、memory
所有的数据都保存在内存中,不需要进行磁盘I/O。Memory表的结构在重启以后还会保留,但数据会丢失。Memroy表在很多场景可以发挥好的作用:
- 用于查找(lookup) 或者映射(mapping) 表,例如将邮编和州名映射的表。
- 用于缓存周期性聚合数据( periodically aggregated data)的结果。
- 用于保存数据分析中产生的中间数据。
4、为什么myISAM比InnoDB快?
- 1. myISAM只缓存了索引块,减少了缓存换入换出的频率。
- 2. myISAM的表结构非聚簇索引,而InnoDB是聚簇索引,InnoDB的二级索引需要找到id回表查一级索引,而myISAM所有的索引直接指向数据行的存储位置offset。
- 3. INNODB还需要维护MVCC一致;虽然你的场景没有,但他还是需要去检查和维护,而myisam 表锁.牺牲了写性能,提高了读性能。
七、执行原理
一条sql语句在innodb底层的执行细节,如图所示:
用例:将id=1 更新为 id=2
- 1、更新数据
- 1-1、将id=1的信息写入回滚段 undo log 中;
- 1-2、undo log在系统表空间或在undo log 表空间,它本身的数据页也在缓冲池中,此时undo log的物理页数据变化也需要写 redo log ,但这不是主流程;
- 1-3、将缓冲池中 id=1改为 id=2;
- 2、将数据页的变化写 redo log 中,redo log根据配置 写:批量顺序写/主动写/被动写 磁盘
- 3、将数据变化SQL写binlog 日志,写入备库成功通过配置决定,写入:收到数据成功/relay log成功/执行成功;
- 4、提交事务;
以上是数据更新的粗略步骤。
八、执行计划
我们首先执行一条sql:
explain select * from user where id =2;
执行后可以看到执行的结果如下:
1、id字段
id表示执行select查询语句的序号,它是sql执行的顺序的标识,sql按照id从大到小执行,id相同的为一组,从上到下执行。
2、type字段
「type字段表示的sql关联的类型或者说是访问的类型」。从这个字段中我们可以确定这条sql查找数据库表的时候,查找记录的大概范围是怎么样的,直接就能体现sql的效率问题。
type字段的类型也是有比较多,主要常见掌握的有以下几个:system、const 、eq_ref 、ref 、range 、index 、ALL。它的性能体现是从高到低。system > const > eq_ref > ref > range > index > ALL,下面就来详细的说一说这属性。
1)system
system是const的特例,「表示表中只有一行记录」,这个几乎不会出现,也作为了解。
2)const
const表示通过索引一次就查找到了数据,一般const出现在「唯一索引或者主键索引中使用等值查询」,因为表中只有一条数据匹配,所以查找的速度很快。如:
explain select * from user where id =2;
3)eq_ref
「eq_ref表示使用唯一索引或者主键索引扫描作为表链接匹配条件,对于每一个索引键,表中只有一条记录与之匹配」。例如:
explain select * from user left join role_user on user.id = role_user.user_id left join role on role_user.role_id=role.id;
4)ref
ref性能比eq_ref差,「也表示表的链接匹配条件,也就是使用哪些表字段作为查询索引列上的值」。ref与eq_ref的区别就是:eq_ref使用的是唯一索引或者主键索引。ref扫描后的结果可能会找到多条符合条件的行数据,本质上是一种索引访问,返回匹配的行。例如:
explain select * from user where name = '张三';
这里type为ALL,说明name这个字段没有走索引,所以我们需要给name字段添加普通索引:
在次执行,可以发现name走了普通索引。
5)range
「range使用索引来检索给定范围的行数据,一般是在where后面使用between、<>、in等查询语句就会出现range」例如:
explain select * from user where id > 2;
6)index
「index表示会遍历索引树」,index回避ALL速度快一些,但是出现index说明需要检查自己的索引是否使用正确。例如:
explain select id from user;
7)ALL
「ALL与index的区别就是ALL是从硬盘中读取,而index是从索引文件中读取」,ALL全表扫描意味着Mysql会从表的头到尾进行扫描,这时候表示通常需要增加索引来进行优化了,或者说是查询中并没有使用索引作为条件进行查询 例如:
explain select * from user ;
3、Extra字段
该字段显示的是sql查询的额外信息,主要有以下几种情况:
- Using index
- Using where
- Using temporary
- Using temporary
- Using join buffer
- Impossible where
- Select tables optimized away
1)Using index
表示查询的列被索引覆盖,这个是查询性能比较高的体现,即所要查询的信息搜在索引里面可以得到,不用回表,索引被正确的使用 例如:
explain select id from user where id =2;
假如同时出现了using where,表示索引用于执行索引键值的查找;若是没有出现using where,则表示索引用于读取数据,而非执行查询的动作。
2)Using index condition
表示只用到了索引下推的条件。即在组合索引中涵盖的列中覆盖了搜索条件,那么会在筛选出来前先用条件过滤一遍,再去回表,减少回表次数。具体索引下推可以去查看innodb特性章节。
3)Using where
该属性与Using index相反,查询的列并没有被索引覆盖,where条件后面使用的是非索引的前导列,它仅仅是使用了where条件而已。例如:
explain select user.* from user,role,role_user where user.id = role_user.user_id and role.id=role_user.role_id;
4)Using temporary
「Using temporary表示使用了临时表存储中间的结果,一般在对结果排序的时候会使用临时表」。例如:排序order by 和分组查询group by。例:
explain select * from (select name from user union select name from role) a where a.name = '张三' ;
5)Using filesort
Using filesort表示文件排序,说明Mysql对数据使用了外部的索引进行排序,并没有使用表中的索引进行排序。例如:
explain select * from user order by name;
6)Using join buffer
Using join buffer表示使用连接缓存。例如:
explain select user.* from user,role,role_user where user.id = role_user.user_id and role.id=role_user.role_id ;
它强调在获取连接条件时,并没有使用索引,而是使用连接缓冲区来存储中间结果,若是出现该值,一般说明需要添加索引来进行优化了。
7)Select tables optimized away
表示select语句没有遍历表或者索引就返回数据了。例如:
explain select min(id) from user;
在Extra字段中还有其它的属性,但是几乎都没见过的,不出现,所以哪些就讲解,有兴趣的可以自己去了解,这里只列出这些常见的。
九、关键字执行过程
1、select
select * from T where age=12 and name like '小%';
像这样一条语句查询,如果走的是age索引,我们的执行流程是这样的。
- 存储引擎在二级索引定位age=12的第一条记录,回表查询到记录完整的信息,返回给server层。
- server层拿到记录后,进行判断该记录name是否满足like ‘小%’ 的条件,如果满足就直接返回给客户端。
这里是不是和我们想象的不一样,以为server层会查到所有结果,统一返回给客户端的,实际上不是,下面再介绍
- server再调用api接口,找存储引擎获取满足age=12的下一条记录,并且在server层判断like ‘小%’ 的条件,将结果返回给客户端。
- 循环往复,直到获取不到下一条记录。
实际上,mysql的结果会写入到net_buffer中,这块内存的大小是参数net_buffer_length决定的,默认16k。重复获取行,直到写满net_buffer就发送,然后继续写。而客户端那边对数据的聚合,是靠mysql的连接驱动来组装结果的。也就是mysql是边写边发的。
2、count
count(0),count(*),count(字段名)有啥区别?这个是我们大家关注的问题。
count(0)相当于是要统计全表的数据,优化器会选择一个非主键索引的最小二级索引。为啥这样呢?因为主键索引里面有记录行,会导致一页中能存储的记录行更少。而二级索引页存储了所有行的信息,只需要遍历更少的额页就能统计出我们需要的全行数。
count(字段名)是统计表中,该字段不为null的所有记录数的总和,逻辑上就不同了。
SELECT COUNT(*) FROM t;
在执行上述查询时,server层会维护一个名叫count的变量,然后:
- 1. server层向InnoDB要第一条记录。
- 2. InnoDB找到idx_key1的第一条二级索引记录,并返回给server层(注意:由于此时只是统计 记录数量,所以并不需要回表)。
- 3. 由于COUNT函数的参数是 * ,MySQL会将 * 当作常数0处理。由于0并不是NULL,server层 给count变量加1。
- 4. server层向InnoDB要下一条记录。
- 5. InnoDB通过二级索引记录的next_record属性找到下一条二级索引记录,并返回给server层。server层继续给count变量加1。
- 6. … 重复上述过程,直到InnoDB向server层返回没记录可查的消息。
- 7. server层将最终的count变量的值发送到客户端。
3、limit
SELECT * FROM t ORDER BY key1 LIMIT5000, 1;
上面这条语句的意思是,以key1正序的结果中,跳过5000行,之后查询1行返回给客户端。如果使用idx_key1执行上述查询,那么MySQL会这样处理:
- 1. server层向InnoDB要第1条记录,InnoDB从idx_key1中获取到第一条二级索引记录,然后进 行回表操作得到完整的聚簇索引记录,然后返回给server层。server层准备将其发送给客户 端,此时发现还有个 LIMIT 5000, 1 的要求,意味着符合条件的记录中的第5001条才可以真 正发送给客户端,所以在这里先做个统计,我们假设server层维护了一个称作limit_count的变 量用于统计已经跳过了多少条记录,此时就应该将limit_count设置为1。
- 2. server层再向InnoDB要下一条记录,InnoDB再根据二级索引记录的next_record属性找到下 一条二级索引记录,再次进行回表得到完整的聚簇索引记录返回给server层。server层在将其 发送给客户端的时候发现limit_count才是1,所以就放弃发送到客户端的操作,将limit_count 加1,此时limit_count变为了2。
- 3. … 重复上述操作
- 4. 直到limit_count等于5000的时候,server层才会真正的将InnoDB返回的完整聚簇索引记录发 送给客户端。
从上述过程中我们可以看到,由于MySQL中是在实际向客户端发送记录前才会去判断LIMIT子句 是否符合要求,所以如果使用二级索引执行上述查询的话,意味着要进行5001次回表操作。server层在进行执行计划分析的时候会觉得执行这么多次回表的成本太大了,还不如直接全表扫描 +filesort快呢,所以就选择了后者执行查询。
1)深翻页解决办法
limit语句的执行效率也太低了,如果提高呢?limit的局限在于需要跳过很多没用的行,如果翻页太深,比如limit 50000,1.需要跳过50000行,那也太坑了,如何避免?
可以借助主键的游标cursor。如果是对一张表的跑批,要翻页多次跑出所有的数据。我们初始方案可能是
SELECT * FROM t ORDER BY id LIMIT 0, 100;--第一页
SELECT * FROM t ORDER BY id LIMIT 100, 100;--第二页
SELECT * FROM t ORDER BY id LIMIT 100, 100;--第三页
采用了游标后,每次都取最新的100条,不需要有跳过的行,具体的数据靠保存上一页的自增id cursor来定位。
SELECT * FROM t where id>0 ORDER BY id LIMIT 0, 100;--第一页
SELECT * FROM t where id>100 ORDER BY id LIMIT 0, 100;--第二页
SELECT * FROM t where id>200 ORDER BY id LIMIT 0, 100;--第三页
使用游标的方案,一定要记得对游标列排序
4、order by
select name,age,city from staff where city = '深圳' order by age limit10;
这样一条sql语句,看一下执行计划,会发现 Extra 这个字段的 Using filesort表示用到排序。
那么它是怎么排序的呢?
1)全字段排序
MySQL 会给每个查询线程分配一块小内存,用于排序的,称为 sort_buffer。什么时候把字段放进去排序呢,其实是通过idx_city索引找到对应的数据,才把数据放进去啦。
上面执行语句,整体的执行流程就是:
- 1. MySQL 为对应的线程初始化sort_buffer,放入需要查询的name、age、city字段;
- 2. 从索引树idx_city, 找到第一个满足 city=’深圳’条件的主键 id,也就是图中的id=9;
- 3. 到主键 id 索引树拿到id=9的这一行数据, 取name、age、city三个字段的值,存到sort_buffer;
- 4. 从索引树idx_city 拿到下一个记录的主键 id,即图中的id=13;
- 5. 重复步骤 3、4 直到city的值不等于深圳为止;
- 6. 前面5步已经查找到了所有city为深圳的数据,在 sort_buffer中,将所有数据根据age进行排序;
- 7. 按照排序结果取前10行返回给客户端。
如果数据太多,内存放不下怎么办?
2)磁盘临时文件辅助排序
实际上,sort_buffer的大小是由一个参数控制的:sort_buffer_size。如果要排序的数据小于sort_buffer_size,排序在sort_buffer 内存中完成,如果要排序的数据大于sort_buffer_size,则借助磁盘文件来进行排序。
使用了磁盘临时文件,整个排序过程又是怎样的呢?
- 1. 从主键Id索引树,拿到需要的数据,并放到sort_buffer内存块中。当sort_buffer快要满时,就对sort_buffer中的数据排序,排完后,把数据临时放到磁盘一个小文件中。
- 2. 继续回到主键 id 索引树取数据,继续放到sort_buffer内存中,排序后,也把这些数据写入到磁盘临时小文件中。
- 3. 继续循环,直到取出所有满足条件的数据。最后把磁盘的临时排好序的小文件,合并成一个有序的大文件。
TPS: 借助磁盘临时小文件排序,实际上使用的是归并排序算法。
3)rowid 排序
rowid 排序就是,只把查询SQL需要用于排序的字段和主键id,放到sort_buffer中。那怎么确定走的是全字段排序还是rowid 排序排序呢?
实际上有个参数控制的。这个参数就是max_length_for_sort_data,它表示MySQL用于排序行数据的长度的一个参数,如果单行的长度超过这个值,MySQL 就认为单行太大,就换rowid 排序。我们可以通过命令看下这个参数取值。
使用rowid 排序的话,整个SQL执行流程又是怎样的呢?
- 1. MySQL 为对应的线程初始化sort_buffer,放入需要排序的age字段,以及主键id;
- 2. 从索引树idx_city, 找到第一个满足 city=’深圳’条件的主键 id,也就是图中的id=9;
- 3. 到主键 id 索引树拿到id=9的这一行数据, 取age和主键id的值,存到sort_buffer;
- 4. 从索引树idx_city 拿到下一个记录的主键 id,即图中的id=13;
- 5. 重复步骤 3、4 直到city的值不等于深圳为止;
- 6. 前面5步已经查找到了所有city为深圳的数据,在 sort_buffer中,将所有数据根据age进行排序;
- 7. 遍历排序结果,取前10行,并按照 id 的值回到原表中,取出city、name 和 age 三个字段返回给客户端。
执行示意图如下:
对比一下全字段排序的流程,rowid 排序多了一次回表。
4)优化思路
我们如何优化order by语句呢?
- 因为数据是无序的,所以就需要排序。如果数据本身是有序的,那就不用排了。而索引数据本身是有序的,我们通过建立联合索引,优化order by 语句。
- 我们还可以通过调整max_length_for_sort_data等参数优化;
比如我们可以建立联合索引idx_city_age,这样查询的数据就不需要用到内存排序了,在索引树上就是我们想要的顺序效果。
5、group by
explain select city ,count(*) as num from staff group by city;
这样一条group by语句:
- Extra 这个字段的Using temporary表示在执行分组的时候使用了临时表
- Extra 这个字段的Using filesort表示使用了排序
group by 怎么就使用到临时表和排序了呢?我们来看下这个SQL的执行流程:
1)简单执行流程
explain select city ,count(*) as num from staff group by city;
我们一起来看下这个SQL的执行流程哈:
- 1. 创建内存临时表,表里有两个字段city和num;
- 2. 全表扫描staff的记录,依次取出city = ‘X’的记录。
- 1)判断临时表中是否有为 city=’X’的行,没有就插入一个记录 (X,1);
- 2)如果临时表中有city=’X’的行的行,就将x 这一行的num值加 1;
- 3. 遍历完成后,再根据字段city做排序,得到结果集返回给客户端。
这个流程的执行图如下:
group by会默认排序,如果不希望排序,可以显示指定不排序。
explain select city ,count(*) as num from staff group by city order by null;
2)松散索引
要利用到松散索引扫描实现 GROUP BY,需要至少满足以下几个条件:
- GROUP BY 条件字段必须在同一个索引中最前面的连续位置(前缀索引);
- 在使用GROUP BY 的同时,只能使用 MAX 和 MIN 这两个聚合函数;
- 如果引用到了该索引中 GROUP BY 条件之外的字段条件的时候,必须以常量形式存在;
为什么松散索引扫描的效率会很高?
因为在没有WHERE子句,也就是必须经过全索引扫描的时候, 松散索引扫描需要读取的键值数量与分组的组数量一样多,也就是说比实际存在的键值数目要少很多。而在WHERE子句包含范围判断式或者等值表达式的时候, 松散索引扫描查找满足范围条件的每个组的第1个关键字,并且再次读取尽可能最少数量的关键字。
3)紧凑索引
和松散索引扫描的区别是需要读取所有满足条件的索引值,之后取数据完成操作。Extra中不显示for group-by 在 MySQL 中,首先会选择尝试通过松散索引扫描来实现 GROUP BY 操作,当发现某些情况无法满足松散索引扫描实现 GROUP BY 的要求之后,才会尝试通过紧凑索引扫描来实现。(比如GROUP BY 条件字段并不连续或者不是索引前缀部分的时候)
4)临时表
当无法找到合适的索引可以利用的时候,就不得不先读取需要的数据,然后通过临时表来完成 GROUP BY 操作。Extra:Using temporary; Using filesort
6、join
mysql的join算法叫做Nested-Loop Join(嵌套循环连接)。而这个Nested-Loop Join有三种变种,下面分别介绍下:
1)Nested-Loop Join的三种变种
a)Simple Nested-Loop
这个算法相当简单、直接。即驱动表中的每一条记录与被驱动表中的记录进行比较判断(就是个笛卡尔积)。对于两表联接来说,驱动表只会被访问一遍,但被驱动表却要被访问到好多遍。
假设R为驱动表,S被驱动表,用伪代码表示一下这个过程就是这样:
for r in R # 扫描R表(驱动表)
for s in S # 扫描S表(被驱动表)
if (r and s satisfy the join condition) # 如果r和s满足join条件
output result # 返回结果集
所以如果R有1万条数据,S有1万条数据,那么数据比较的次数1万 * 1万 =1亿次,这种查询效率会非常慢。
b)Index Nested-Loop
这个是基于索引进行连接的算法。它要求被驱动表上有索引,可以通过索引来加速查询。
假设R为驱动表,S被驱动表,用伪代码表示一下这个过程就是这样:
For r in R # 扫描R表
for s in Sindex # 查询S表的索引(固定3~4次IO,B+树高度)
if (s == r) # 如果r匹配了索引s
output result # 返回结果集
c)Block Nested-Loop
这个算法较Simple Nested-Loop Join的改进就在于可以减少被驱动表的扫描次数。因为它使用Join Buffer来减少内部循环读取表的次数。
假设R为驱动表,S被驱动表,用伪代码表示一下这个过程就是这样:
for r in R # 扫描表R
store p from R in Join Buffer # 将部分或者全部R的记录保存到Join Buffer中,记为p
for s in S # 扫描表S
if (p and s satisfy the join condition) # p与s满足join条件
output result # 返回为结果集
可以看到相比Simple Nested-Loop Join算法,Block Nested-LoopJoin算法仅多了一个所谓的Join Buffer
2)为什么这样就能减少被驱动表的扫描次数呢?
下图相比更好地解释了Block Nested-Loop Join算法的运行过程
可以看到Join Buffer用以缓存联接需要的列(所以再次提醒我们,最好不要把*作为查询列表,只需要把我们关心的列放到查询列表就好了,这样还可以在join buffer中放置更多的记录呢,是不是这个道理哈,哈哈)
然后以Join Buffer批量的形式和被驱动表中的数据进行联接比较。
如果被驱动表的数据超过了join buffer的size,那么就会把它分成多个join buffer文件块,每条驱动表数据都和多个块数据联表。
3)关于Join Buffer
- Join Buffer会缓存所有参与查询的列而不是只有Join的列。
- join_buffer_size的默认值是256K
4)Join算法的优先级
在选择Join算法时,会有优先级:
Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join
当不使用Index Nested-Loop Join的时候,默认使用Block Nested-Loop Join。
使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。
5)Join优化
通过上面的简单介绍,可以总结出以下几种优化思路:
- 1. 用小结果集驱动大结果集,减少外层循环的数据量
- 2. 如果小结果集和大结果集连接的列都是索引列,mysql在join时也会选择用小结果集驱动大结果集,因为索引查询的成本是比较固定的,这时候外层的循环越少,join的速度便越快。
- 3. 为匹配的条件增加索引:争取使用Index Nested-Loop Join,减少内层表的循环次数
- 4. 增大join buffer size的大小:当使用Block Nested-Loop Join时,一次缓存的数据越多,那么外层表循环的次数就越少,减少不必要的字段查询:
- 5. 当用到Block Nested-Loop Join时,字段越少,join buffer 所缓存的数据就越多,外层表的循环次数就越少;