索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

前言

Hello我又来了,快年末了,做为一个有抱负的码农,我想给本身攒一个年终总结。自上上篇写了手动搭建Redis集群和MySQL主从同步(非Docker)和上篇写了动手实现MySQL读写分离and故障转移以后,索性此次把数据库中最核心的也是最难搞懂的内容,也就是索引,分享给你们。html

这篇博客我会谈谈对于索引结构我本身的见解,以及分享如何从零开始一层一层向上最终理解索引结构。java

从一个简单的表开始

create table user(
    id int primary key,
    age int,
    height int,
    weight int,
    name varchar(32)
)engine = innoDb;

 

相信只要入门数据库的同窗均可以理解这个语句,咱们也将从这个最简单的表开始,一步步地理解MySQL的索引结构。sql

首先,咱们往这个表中插入一些数据。数据库

INSERT INTO user(id,age,height,weight,name)VALUES(2,1,2,7,'小吉');
INSERT INTO user(id,age,height,weight,name)VALUES(5,2,1,8,'小尼');
INSERT INTO user(id,age,height,weight,name)VALUES(1,4,3,1,'小泰');
INSERT INTO user(id,age,height,weight,name)VALUES(4,1,5,2,'小美');
INSERT INTO user(id,age,height,weight,name)VALUES(3,5,6,7,'小蔡');

 

咱们来查一下,看看这些数据是否已经放入表中。数据结构

select * from user;

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

能够看到,数据已经完整地放到了咱们建立的user表中。post

可是不知道你们发现了什么没有,好像发生了一件很是诡异的事情,咱们插入的数据好像乱序了…性能

MySQL好像悄悄的给咱们按照id排了个序。大数据

为何会出现MySQL在咱们没有显式排序的状况下,默默帮咱们排了序呢?它是在何时进行排序的?优化

页的引入

不知道你们毕业多长时间了,做为一个刚学完操做系统不久的学渣,页的概念依旧在脑中尚未变凉。其实MySQL中也有相似页的逻辑存储单位,听我慢慢道来。spa

在操做系统的概念中,当咱们往磁盘中取数据,假设要取出的数据的大小是1KB,可是操做系统并不会只取出这1kb的数据,而是会取出4KB的数据,由于操做系统的一个页表项的大小是4KB。那为何咱们只须要1KB的数据,可是操做系统要取出4KB的数据呢?

这就涉及到一个程序局部性的概念,具体的概念我背不清了,大概就是“一个程序在访问了一条数据以后,在以后会有极大的可能再次访问这条数据和访问这条数据的相邻数据”,因此索性直接加载4KB的数据到内存中,下次要访问这一页的数据时,直接从内存中找,能够减小磁盘IO次数,咱们知道,磁盘IO是影响程序性能主要的因素,由于磁盘IO和内存IO的速度是不可同日而语的。

或许看完上面那一大段描述,仍是有些抽象,因此咱们索性回到数据库层面中,从新理解页的概念。

抛开全部东西不谈,假设仍是咱们刚才插入的那些数据,咱们如今要找id = 5的数据,依照最原始的方式,咱们必定会想到的就是——遍历,没错,这也是咱们刚开始学计算机的时候最经常使用的寻找数据的方式。那么咱们就来看看,以遍历的方式,咱们找到id=5的数据,须要经历几回磁盘IO。

首先,咱们得先从id=1的数据开始读起,而后判断是不是咱们须要的数据,若是不是,就再取id=2的数据,再进行判断,循环往复。毋庸置疑,在MySQL帮咱们排好序以后,咱们须要经历五次磁盘IO,才能将5号数据找到并读出来。

那么咱们再来看看引入页的概念以后,咱们是如何读数据的。

在引入页的概念以后,MySQL会将多条数据存在一个叫“页”的数据结构中,当MySQL读取id=1的数据时,会将id=1数据所在的页整页读到内存中,而后在内存中进行遍历判断,因为内存的IO速度比磁盘高不少,因此相对于磁盘IO,几乎能够忽略不计,那么咱们来看看这样读取数据咱们须要经历几回磁盘IO(假设每一页能够存4条数据)。

 

那么咱们第一次会读取id=1的数据,而且将id=1到id=4的数据所有读到内存中,这是第一次磁盘IO,第二次将读取id=5的数据到内存中,这是第二次磁盘IO。因此咱们只须要经历2次磁盘IO就能够找到id=5的这条数据。

但其实,在MySQL的InnoDb引擎中,页的大小是16KB,是操做系统的4倍,而int类型的数据是4个字节,其它类型的数据的字节数一般也在4000字节之内,因此一页是能够存放不少不少条数据的,而MySQL的数据正是以页为基本单位组合而成的

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

上图就是咱们目前为止所理解的页的结构,他包含咱们的多条数据,另外,MySQL的数据以页组成,那么它有指向下一页的指针和指向上一页的指针。

那么说到这里,其实能够回答第一个问题了,MySQL实际上就是在咱们插入数据的时候,就帮咱们在页中排好了序,至于为何要排序,这里先卖个关子,接着往下看。

排序对性能的影响

上文中咱们提了一个问题,为何数据库在插入数据时要对其进行排序呢?咱们按正常顺序插入数据不是也挺好的吗?

这就要涉及到一个数据库查询流程的问题了,不管如何,咱们是绝对不会去无缘无故地在插入数据时增长一个操做来让流程复杂化的,因此插入数据时排序必定有其目的,就是优化查询的效率

而咱们不难看出,页内部存放数据的模块,实质上就是一个链表的结构,链表的特色也就是增删快,查询慢,因此优化查询的效率是必须的。

基于单页模式存储的查询流程

仍是基于咱们第一节中的那张页图来谈,咱们插入了五条数据,id分别是从1-5,那么假设我要找一个表中不存在的id,假设id=-1,那么如今的查询流程就是:

将id=1的这一整页数据取出,进行逐个比对,那么当咱们找到id=1的这条数据时,发现这个id大于咱们所须要找的哪一个id,因为数据库在插入数据时,已经进行过排序了,那么在id=1的数据后面,都是id>1的数据,因此咱们就不须要再继续往下寻找了。

若是在插入时没有进行排序,那毋庸置疑,咱们须要再继续往下进行寻找,逐条查找直到到结尾也没有找到这条数据,才能返回不存在这条数据。

固然,这只是排序优化的冰山一角,接着往下看。

上述页模式可能带来的问题

说完了排序,下面就来分析一下咱们在第一节中的那幅图,对于大数据量下有什么弊端,或者换一个说法,咱们能够怎么对这个模式进行优化。

咱们不难看出,在现阶段咱们了解的页模式中,只有一个功能,就是在查询某条数据的时候直接将一整页的数据加载到内存中,以减小硬盘IO次数,从而提升性能。可是,咱们也能够看到,如今的页模式内部,其实是采用了链表的结构,前一条数据指向后一条数据,本质上仍是经过数据的逐条比较来取出特定的数据。

那么假设,咱们这一页中有一百万条数据,咱们要查的数据正好在最后一个,那么咱们是否是必定要从前日后找到这一条数据呢?若是是这样,咱们须要查找的次数就达到了一百万次,即便是在内存中查找,这个效率也是不高的。那么有什么办法来优化这种状况下的查找效率呢?

页目录的引入

咱们能够打个比方,咱们在看书的时候,若是要找到某一节,而这一节咱们并不知道在哪一页,咱们是否是就要从前日后,一节一节地去寻找咱们须要的内容的页码呢?答案是否认的,由于在书的前面,存在目录,它会告诉你这一节在哪一页,例如,第一节在第1页、第二节在第13页。在数据库的页中,实际上也使用了这种目录的结构,这就是页目录。

那么引入页目录以后,咱们所理解的页结构,就变成了这样:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

分析一下这张图,实际上页目录就像是咱们在看书的时候书本的目录同样,目录项1就至关于第一节,目录项2就至关于第二节,而每一条数据就至关于书本的每一页,这张图就能够解释成,第一节从第一页开始,第二节从第三页开始,而实际上,每一个目录项会存放本身这个目录项当中最小的id,也就是说,目录项1中会存放1,而目录项2会存放3。

那么对比一下数据库在没有页目录时候的查找流程,假设要查找id=3的数据,在没有页目录的状况下,须要查找id=一、id=二、id=3,三次才能找到该数据,而若是有页目录以后,只须要先查看一下id=3存在于哪一个目录项下,而后直接经过目录项进行数据的查找便可,若是在该目录项下没有找到这条数据,那么就能够直接肯定这条数据不存在,这样就大大提高了数据库的查找效率,可是这种页目录的实现,首先就须要基于数据是在已经进行过排序的的场景下,才能够发挥其做用,因此看到这里,你们应该明白第二个问题了,为何数据库在插入时会进行排序,这才是真正发挥排序的做用的地方。

页的扩展

在上文中,咱们基本上说明白了MySQL数据库中页的概念,以及它是如何基于页来减小磁盘IO次数的,以及排序是如何优化查询的效率的。

那么咱们如今再来思考第三个问题:在开头说页的概念的时候,咱们有说过,MySQL中每一页的大小只有16KB,不会随着数据的插入而自动扩容,因此这16KB不可能存下咱们全部的数据,那么一定会有多个页来存储数据,那么在多页的状况下,MySQL中又是怎么组织这些页的呢?

针对这个问题,咱们继续来画出咱们如今所了解的多页的结构图:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

能够看到,在数据不断变多的状况下,MySQL会再去开辟新的页来存放新的数据,而每一个页都有指向下一页的指针和指向上一页的指针,将全部页组织起来(这里修改了一下数据,将每一列的数据都放到了数据区中,其中第一个空格以前的表明id),第一页中存放id为1-5的数据,第二页存放id为6-10的数据,第三页存放id为11-15的数据,须要注意的是在开辟新页的时候,咱们插入的数据不必定是放在新开辟的页上,而是要进行全部页的数据比较,来决定这条插入的数据放在哪一页上,而完成数据插入以后,最终的多页结构就会像上图中画的那样。

多页模式

在多页模式下,MySQL终于能够完成多数据的存储了,就是采用开辟新页的方式,将多条数据放在不一样的页中,而后一样采用链表的数据结构,将每一页链接起来。那么能够思考第四个问题:多页状况下是否对查询效率有影响呢?

多页模式对于查询效率的影响

针对这个问题,既然问出来了,那么答案是确定的,多页会对查询效率产生必定的影响,影响主要就体如今,多页其本质也是一个链表结构,只要是链表结构,查询效率必定不会高。

假设数据又很是多条,数据库就会开辟很是多的新页,而这些新页就会像链表同样链接在一块儿,当咱们要在这么多页中查询某条数据时,它仍是会从头节点遍历到存在咱们要查找的那条数据所存在的页上,咱们好不容易经过页目录优化了页中数据的查询效率,如今又出现了以页为单位的链表,这不是前功尽弃了吗?

如何优化多页模式

因为多页模式会影响查询的效率,那么确定须要有一种方式来优化多页模式下的查询。相信有同窗已经猜出来了,既然咱们能够用页目录来优化页内的数据区,那么咱们也能够采起相似的方式来优化这种多页的状况。

是的,页内数据区和多页模式本质上都是链表,那么的确能够采用相同的方式来对其进行优化,它就是目录页。

因此咱们对比页内数据区,来分析如何优化多页结构。在单页时,咱们采用了页目录的目录项来指向一行数据,这条数据就是存在于这个目录项中的最小数据,那么就能够经过页目录来查找所需数据。

因此对于多页结构也能够采用这种方式,使用一个目录项来指向某一页,而这个目录项存放的就是这一页中存放的最小数据的索引值。和页目录不一样的地方在于,这种目录管理的级别是页,而页目录管理的级别是行。

那么分析到这里,咱们多页模式的结构就会是下图所示的这样:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

存在一个目录页来管理页目录,目录页中的数据存放的就是指向的那一页中最小的数据。

这里要注意的一点是:其实目录页的本质也是页,普通页中存的数据是项目数据,而目录页中存的数据是普通页的地址。

假设咱们要查找id=19的数据,那么按照之前的查找方式,咱们须要从第一页开始查找,发现不存在那么再到第二页查找,一直找到第四页才能找到id=19的数据,可是若是有了目录页,就可使用id=19与目录页中存放的数据进行比较,发现19大于任何一条数据,因而进入id=16指向的页进行查找,直接而后再经过页内的页目录行级别的数据的查找,很快就能够找到id为19的数据了。随着数据愈来愈多,这种结构的效率相对于普通的多页模式,优点也就愈来愈明显。

回归正题,相信有对MySQL比较了解的同窗已经发现了,咱们画的最终的这幅图,就是MySQL中的一种索引结构——B+树。

B+树的引入

B+树的特色我在《[从入门到入土]使人脱发的数据库底层设计》已经有详细叙述过了,在这里就不重复叙述了,若是有不了解的同窗能够去看这篇博客。

咱们接着往下聊,咱们将咱们画的存在目录页的多页模式图宏观化,能够造成下面的这张图:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

这就是咱们兜兜转转由简到繁造成的一颗B+树。和常规B+树有些许不一样,这是一棵MySQL意义上的B+树,MySQL的一种索引结构,其中的每一个节点就能够理解为是一个页,而叶子节点也就是数据页,除了叶子节点之外的节点就是目录页。

这一点在图中也能够看出来,非叶子节点只存放了索引,而只有叶子节点中存放了真实的数据,这也是符合B+树的特色的。

B+树的优点

  • 因为叶子节点上存放了全部的数据,而且有指针相连,每一个叶子节点在逻辑上是相连的,因此对于范围查找比较友好。

  • B+树的全部数据都在叶子节点上,因此B+树的查询效率稳定,通常都是查询3次。

  • B+树有利于数据库的扫描。

  • B+树有利于磁盘的IO,由于他的层高基本不会由于数据扩大而增高(三层树结构大概能够存放两千万数据量。

页的完整结构

说完了页的概念和页是如何一步一步地组合称为B+树的结构以后,相信你们对于页都有了一个比较清楚的认知,因此这里就要开始说说官方概念了,基于咱们上文所说的,给出一个完整的页结构,也算是对上文中本身理解页结构的一种补充。

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

上图为 Page 数据结构,File Header 字段用于记录 Page 的头信息,其中比较重要的是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 字段,经过这两个字段,咱们能够找到该页的上一页和下一页,实际上全部页经过两个字段能够造成一条双向链表。

Page Header 字段用于记录 Page 的状态信息。接下来的 Infimum 和 Supremum 是两个伪行记录,Infimum(下确界)记录比该页中任何主键值都要小的值,Supremum (上确界)记录比该页中任何主键值都要大的值,这个伪记录分别构成了页中记录的边界。

 

User Records 中存放的是实际的数据行记录,具体的行记录结构将在本文的第二节中详细介绍。Free Space 中存放的是空闲空间,被删除的行记录会被记录成空闲空间。Page Directory 记录着与二叉查找相关的信息。File Trailer 存储用于检测数据完整性的校验和等数据。

引用来源:https://www.cnblogs.com/bdsir/p/8745553.html

基于B+树聊聊MySQL的其它知识点

看到这里,咱们已经了解了MySQL从单条数据开始,到经过页来减小磁盘IO次数,而且在页中实现了页目录来优化页中的查询效率,而后使用多页模式来存储大量的数据,最终使用目录页来实现多页模式的查询效率并造成咱们口中的索引结构——B+树。既然说到这里了,那咱们就来聊聊MySQL的其余知识点。

聚簇索引和非聚簇索引

关于聚簇索引和非聚簇索引在[从入门到入土]使人脱发的数据库底层设计这篇文章中已经有了详细的介绍,这里简单地说说,所谓聚簇索引,就是将索引和数据放到一块儿,找到索引也就找到了数据,咱们刚才看到的B+树索引就是一种聚簇索引,而非聚簇索引就是将数据和索引分开,查找时须要先查找到索引,而后经过索引回表找到相应的数据。InnoDB有且只有一个聚簇索引,而MyISAM中都是非聚簇索引。

联合索引的最左前缀匹配原则

在MySQL数据库中不只能够对某一列创建索引,还能够对多列创建一个联合索引,而联合索引存在一个最左前缀匹配原则的概念,若是基于B+树来理解这个最左前缀匹配原则,相对来讲就会容易很不少了。

首先咱们基于文首的这张表创建一个联合索引:

create index idx_obj on user(age asc,height asc,weight asc)

咱们已经了解了索引的数据结构是一颗B+树,也了解了B+树优化查询效率的其中一个因素就是对数据进行了排序,那么咱们在建立idx_obj这个索引的时候,也就至关于建立了一颗B+树索引,而这个索引就是依据联合索引的成员来进行排序,这里是age,height,weight。

看过我以前那篇博客的同窗知道,InnoDB中只要有主键被定义,那么主键列被做为一个聚簇索引,而其它索引都将被做为非聚簇索引,因此天然而然的,这个索引就会是一个非聚簇索引。

因此根据这些咱们能够得出结论:

  • idx_obj这个索引会根据age,height,weight进行排序

  • idx_obj这个索引是一个非聚簇索引,查询时须要回表

根据这两个结论,首先须要了解的就是,如何排序?

单列排序很简单,比大小嘛,谁都会,可是多列排序是基于什么原则的呢(重点)?

实际上在MySQL中,联合索引的排序有这么一个原则,从左往右依次比较大小,就拿刚才创建的索引举例子,他会先去比较age的大小,若是age的大小相同,那么比较height的大小,若是height也没法比较大小, 那么就比较weight的大小,最终对这个索引进行排序。

那么根据这个排序咱们也能够画出一个B+树,这里就不像上文画的那么详细了,简化一下:

数据:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

B+树:

索引很难么?带你从头至尾捋一遍MySQL索引结构,不信你学不会!

注意:此时因为是非聚簇索引,因此叶子节点不在有数据,而是存了一个主键索引,最终会经过主键索引来回表查询数据。

B+树的结构有了,就能够经过这个来理解最左前缀匹配原则了。

咱们先写一个查询语句

SELECT * FROM user WHERE age=and height = and weight = 7

毋庸置疑,这条语句必定会走idx_obj这个索引。

那么咱们再看一个语句:

SELECT * FROM user WHERE height=and weight = 7

思考一下,这条SQL会走索引吗?

答案是否认的,那么咱们分析的方向就是,为何这条语句不会走索引。

上文中咱们提到了一个多列的排序原则,是从左到右进行比较而后排序的,而咱们的idx_obj这个索引从左到右依次是age,height,weight,因此当咱们使用height和weight来做为查询条件时,因为age的缺失,那么就没法从age来进行比较了。

看到这里可能有小伙伴会有疑问,那若是直接用height和weight来进行比较不能够吗?显然是不能够的,能够举个例子,咱们把缺失的这一列写做一个问号,那么这条语句的查询条件就变成了?27,那么咱们从这课B+树的根节点开始,根节点上有127和365,那么以height和weight来进行比较的话,走的必定是127这一边,可是若是缺失的列数字是大于3的呢?好比427,527,627,那么若是走索引来查询数据,将会丢失数据,错误查询。因此这种状况下是绝对不会走索引进行查询的。这就是最左前缀匹配原则的成因。

  1. 最左前缀匹配原则,MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)就中止匹配,好比 a=3 and b=4 and c>5 and d=6,若是创建(a,b,c,d)顺序的索引,d是没法使用索引的,若是创建(a,b,d,c)的索引则均可以使用到,a、b、d的顺序能够任意调整。

  2. =和in能够乱序,好比 a=1 and b=2 and c=3 创建(a,b,c)索引能够任意顺序,MySQL的查询优化器会帮你优化成索引能够识别的形式。

根据咱们了解的能够得出结论:

只要没法进行排序比较大小的,就没法走联合索引。

能够再看几个语句:

SELECT * FROM user WHERE age=and height = 2

这条语句是能够走idx_obj索引的,由于它能够经过比较 (12?<365)。

SELECT * FROM user WHERE age=and weight=7

这条语句也是能够走ind_obj索引的,由于它也能够经过比较(1?7<365),走左子树,可是实际上weight并无用到索引,由于根据最左匹配原则,若是有两页的age都等于1,那么会去比较height,可是height在这里并不做为查询条件,因此MySQL会将这两页全都加载到内存中进行最后的weight字段的比较,进行扫描查询。

SELECT * FROM user where age>1

这条语句不会走索引,可是能够走索引。这句话是什么意思呢?这条SQL很特殊,因为其存在能够比较的索引,因此它走索引也能够查询出结果,可是因为这种状况是范围查询而且是全字段查询,若是走索引,还须要进行回表,MySQL查询优化器就会认为走索引的效率比全表扫描还要低,因此MySQL会去优化它,让他直接进行全表扫描。

SELECT * FROM user WEHRE age=and height>and weight=7

这条语句是能够走索引的,由于它能够经过age进行比较,可是weight不会用到索引,由于height是范围查找,与第二条语句相似,若是有两页的height都大于2,那么MySQL会将两页的数据都加载进内存,而后再来经过weight匹配正确的数据。

为何InnoDB只有一个聚簇索引,而不将全部索引都使用聚簇索引?

由于聚簇索引是将索引和数据都存放在叶子节点中,若是全部的索引都用聚簇索引,则每个索引都将保存一份数据,会形成数据的冗余,在数据量很大的状况下,这种数据冗余是很消耗资源的。

补充两个关于索引的点

这两个点也是上次写关于索引的博客时漏下的,这里补上。

1.什么状况下会发生明明建立了索引,可是执行的时候并无经过索引呢?

科普时间:查询优化器 一条SQL语句的查询,能够有不一样的执行方案,至于最终选择哪一种方案,须要经过优化器进行选择,选择执行成本最低的方案。

在一条单表查询语句真正执行以前,MySQL的查询优化器会找出执行该语句全部可能使用的方案,对比以后找出成本最低的方案。这个成本最低的方案就是所谓的执行计划。

优化过程大体以下:

一、根据搜索条件,找出全部可能使用的索引
二、计算全表扫描的代价
三、计算使用不一样索引执行查询的代价
四、对比各类执行方案的代价,找出成本最低的那一个 。

参考:https://juejin.im/post/5d23ef4ce51d45572c0600bc

根据咱们刚才的那张表的非聚簇索引,这条语句就是因为查询优化器的做用,形成没有走索引:

SELECT * FROM user where age>1

2.在稀疏索引状况下一般须要经过叶子节点的指针回表查询数据,什么状况下不须要回表?

科普时间:覆盖索引 覆盖索引(covering index)指一个查询语句的执行只用从索引中就可以取得,没必要从数据表中读取。也能够称之为实现了索引覆盖。

当一条查询语句符合覆盖索引条件时,MySQL只须要经过索引就能够返回查询所须要的数据,这样避免了查到索引后再返回表操做,减小I/O提升效率。

如,表covering_index_sample中有一个普通索引 idx_key1_key2(key1,key2)。当咱们经过SQL语句:select key2 from covering_index_sample where key1 = 'keytest';的时候,就能够经过覆盖索引查询,无需回表。

参考:https://juejin.im/post/5d23ef4ce51d45572c0600bc

例如:

SELECT age FROM user where age = 1

这句话就不须要进行回表查询。

结语

本篇文章着重聊了一下关于MySQL的索引结构,从零开始慢慢构建了一个B+树索引,而且根据这个过程谈了B+树是如何一步一步去优化查询效率的。

简单地概括一下就是:

排序:优化查询的根本,插入时进行排序实际上就是为了优化查询的效率。
页:用于减小IO次数,还能够利用程序局部性原理,来稍微提升查询效率。
页目录:用于规避链表的软肋,避免在查询时进行链表的扫描。
多页:数据量增长的状况下开辟新页来保存数据。
目录页:“特殊的页目录”,其中保存的数据是页的地址。查询时能够经过目录页快速定位到页,避免多页的扫描。

欢迎访问博客:http://blog.objectspace.cn/

相关文章
相关标签/搜索