InnoDB由行到页再到索引

Posted 百里东君~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了InnoDB由行到页再到索引相关的知识,希望对你有一定的参考价值。

本文我们将从底层讲一讲mysql中InnoDB的原理结构,从行讲到页,再讲到索引,看完这篇文章,我们将对索引有一定的了解,这对我们平时写sql和进行慢sql优化有很大的帮助。

一、行

MySQL有多种行存储格式,5.7之后默认是Dynamic,他和之前版本默认的compact行格式基本相同,我们先看一下compact行格式的结构

其中记录的真实数据就是我们平时通过select语句看到的表数据,但是表中的每一行数据在底层还有额外的存储信息,例如

变长字段列表:变长字段实际占⽤字节数按逆序排列,因为有一些列可能是varchar类型,计算机读取列数据的时候可以通过该记录列表知道某一指定列的数据具体需要读取多少字节。
NULL值列表:存储允许NULL的列对应⼀个⼆进制位图,1表⽰为NULL,逆序排列,同样,如果我门在建表的时候没有指定该列为not null,而mysql中的null又是一个数据类型,所以要记录下来,这里注意,mysql判断某一列是否为空,不能直接“where 列=null”,而是应该使用“where 列 is null”或者“where 列 is not null”
记录头信息:⽤于描述记录的记录头信息,它是由固定的5个字节组成

这里重点讲一下记录头信息,如图


其中delete_mask可以讲一下,它用0或者1来记录该行数据是否被删除,因为当删除数据时,mysql并不会回收被已删除数据的占据的存储空间,以及索引位。而是空在那里,将delete_mask置为1表示删除,等待新的数据来弥补这个空缺,这样就有一个缺点,如果一时半会,没有数据来填补这个空缺,就显得太浪费资源。所以对于写比较频繁的表,可以选择定期进行optimize,进行优化内存中的碎片空间,学过计算机和操作系统的都知道,大量的内存碎片其实很影响性能,而下面的命令能优化存储空间。
mysql> optimize table ad_visit_history;

二、页

简单了解行之后,接下来我们由浅到深认识页,如果给你100条排好序的数据让你找出id为12的一条数据,那么如果我们用数组存储的话,我们一般就是遍历数组找出,或者用二分查找法,那么我们是否可以通过数据结构来提高查询速度。
1、假设有100条数据,每十条数据分为一组

2、每一组都配置一个槽,每个槽记录了每组最大的那条记录的地址

那么此时,我们可以通过二分法快速查找记录啦:

假设现在就 10 组数据,然后我要找 ID 等于 12 这条数据,我就:
先计算中间槽的位置(1+10)/2=5,通过地址找到第五组,此时第五组 ID 是 50,12<50,所以继续二分。
(1+5)/2=3,通过地址找到第三组,此时第三组 ID 是 30,12<30,所以继续二分。
(1+3)/2=2,通过地址找到第二组,此时第二组 ID 是 20,12<20,所以继续二分。
(1+2)/2=1,通过地址找到第一组,此时第一组 ID 是 10,12>10,现在能断定 12 在第二组。

总结: 1、先通过二分法找到数据所在的槽。
2、然后再通过单向链表遍历得到数据。

从图中看起来第一组和第二组是分开的?实际上地址是相连的,所以通过第一组最后一条数据,可以往后随着单向链表遍历,即id为10头信息中有一个地址指向id为11。上面只是为了大家跟方便理解,其实真实的存储结构如下图:

3、一千个数据为一个界限来分割数据,我们将每一千个数据所组成的结构称之为页,每页之间用双向链表相连

4、页多了,我们还需要专门搞了一个特别的页,页里面的存储着也好,这个特别的页就是目录,称它为目录页

比如:我现在要找 ID 为 500 的那条数据。
我们遍历目录页数据就可以知道该条数据在页 1,然后就开始在页内通过二分来查找数据在页内哪个槽,再遍历槽指向的分组进行遍历
5、当数据量继续加大,目录页也可以和数据页一样,进行新增页

目录页多了,查找目录页也会变慢,这时候也出现了根目录:

我们上面的图是一种树形结构,相信大家也猜出这就是B+树的结构,没错,在mysql中就是一种索引树结构。
假设:数据页每页存放100条⽤户记录,索引页每页存放1000条索引记录(主键ID和页号)
⼀层:存放100条⽤户记录
两层:100 * 1000 = 10W
三层:10010001000=1亿
四层:10010001000*1000=1000亿

当然,我们还是得了解页的结构:



我们平时插入数据的时候一般都会在建表时候设置表主键自增,大家有没有思考过为什么呢?

我们根据前面的索引树的图可以知道,数据行是按照id进行索引排序,如果我们id不是自增,有一种极端情况就是,当我们插入一条数据,经过索引确定了数据该插入的页号,然后发现该页已经满了,这个时候页就要将最大的数据进行分裂到下一页中,再将数据插入,但是下一页之后的每一页都是满的,那么这条插入的数据将会发起级联反应,导致每一页都需要进行数据操作,造成数据库写入的性能极其低下。

三、索引

我们想要查看一条sql查询执行的效率,可以在sql前面加上explain,例如: explain select * from table_1
本篇文章,我们先从单表进行分析
单表访问的方式主要2中
1、全表扫描 ALL
2、索引查询 (这里只讲最常见的4种)

  • 主键或者唯一索引列const
  • 普通二级索引与常数进行等值比较ref
  • 索引列范围查询range
  • 全索引页扫描index

const

意思是常数级别的,代价是可以忽略不计的
主键或者唯一二级索引是由多个列构成的话,索引中的每一个列和常数的比较都只能是等值比较,才满足const。因为只有该索引中全部列都采用等值比较才可以定位唯一的一条记录。
可以通过建索引PRIMARY KEY,或者UNIQUE KEY。

ref

把搜索条件为二级索引列与常数的等值比较的访问方法称为ref

从图示中可以看出,对于普通的二级索引来说,通过索引列进行等值比较后可能匹配到多条连续的记录,而不是像主键或者唯一二级索引那样最多只能匹配1条记录,所以这种ref访问方法比const差了那么一丢丢,但是在二级索引等值比较时匹配的记录数较少时的效率还是很高的
可以通过建索引NORMAL KEY

range

如果采用二级索引 + 回表的方式来执行的话,把这种利用索引进行范围匹配的访问方法称之为:range。
注:此处所说的使用索引进行范围匹配中的索引 可以是聚簇索引,也可以是二级索引。

in > < >= <= between and 索引列只要有这些符号,基本就是range访问

注:
在查询数据条数约占总条数五分之一以下时能够使用到索引,但超过五分之一时,则使用全表扫描了。

index

在二级索引b+树中直接查出需要的索引列,不用再次回表查询
UNIQUE KEY index_part1_part2_part3 (key_part1,key_part2,key_part3) USING BTREE
SELECT key_part1, key_part2, key_part3 FROM single_table WHERE key_part2 = ‘abc’;

key_part1, key_part2, key_part3三个字段是联合索引index_part1_part2_part3中字段,虽然
key_part2不是最左边列,但在二级索引b+树中有该字段,且select显示的三个列都是位于二级索引B+树中, 不用回表查询,速度也是挺快的。

接下来,我们来重点讲一下联合索引
KEY 联合索引 (姓名生日成绩) USING BTREE

接下来我们看一组sql

其中第二条sql是不走索引的,我们从索引树种可以看到,如果我们只看生日这一整行,生日并不是有序的,只有当名字相同下的生日才是有序的,而该查询显然只能全表查询。但是第3条sql则可以利用索引进行查询,为什么呢?
这里我们要了解一个名词ICP

四、索引优化

1、ICP

icp是mysql底层的一种优化方式,这里举一个梨子,例如下图中的一个sql查询,我们一般实际生产中如果没有利用联合索引的话,会有方案一的优化,但是mysql会充分利用联合索引的优势,来办我们优化sql的查询效率,即方案二。

2、MRR

如果我们需要查询一组id的数据,我们通常是sql中用到了 id in(),这样的查询条件,但是如果我们的id非常多且无序,那么mysql不经过优化进行查询的话,在树结构进行查询会不断重复扫描,效率可能不及全表扫描,所以mysql底层会有mrr这种优化方案。

以上是关于InnoDB由行到页再到索引的主要内容,如果未能解决你的问题,请参考以下文章

索引优化策略

mysql索引的性能分析

mysql btree和hash索引对比

为什么MyISAM会比Innodb的查询速度快

vim常用命令

Git 通过 grep/regex 添加行到索引