不懂数据库索引的底层原理?那是由于你内心没点b树

本文在我的技术博客不一样步发布,详情可用力戳
亦可扫描屏幕右侧二维码关注我的公众号,公众号内有我的联系方式,等你来撩...mysql

  前几天下班回到家后正在处理一个白天没解决的bug,厕所忽然传来对象的声音:
  对象:xx,你有《时间简史》吗?
  我:我去!妹子,你这啥癖好啊,我有时间也不会去捡屎啊!
  对象:...人家说的是霍金的科普著做《时间简史》,是一本书啦!
  我:哦,那我没有...
  对象:人家想看诶,你明天帮我去图书馆借一本吧...
  我:我明天还要改...
  对象:你是否是不爱我了,分手!
  我:我一大早就去~sql

  次日一大早我就到了图书馆,刚进门就看到一个索引牌,标识着不一样楼层的功能,这样我很快能定位到我要找的目标所在的楼层了。数据库

  

  我到楼上后又看到每排的书架上又对书的分类进行了细分,这样我能更快的定位到我要找的书具体在哪一个书架!数组

  而且每一个楼层都有一台查询终端,输入书名就能查到对应的惟一标识“索书号”,相似于P159-49/164这样的一个编码,书架上的书都是按照这个编码进行排序的!有了这个编码再去对应的书架上,很快就能找到对应的书在书架的具体位置了。网络

  

  不到十分钟,我就从图书馆借好书出来了。数据结构

  这么大的图书馆,我为何能在这么短的时间内找到我要的书?若是这些书是杂乱无章的堆放,或者没有任何标识的放在书架,我还能这么快的找到吗?oracle

  这不由让我想到了咱们开发中用到的数据库,图书馆的书就相似咱们数据表中的数据,楼层索引牌、书架分类标识、索书号就相似咱们查找数据的索引。sqlserver

  那咱们经常使用的数据库的索引底层的一个数据结构是什么样的呢?想到这里我又回到图书馆借了一本《数据库从入门到放弃》!性能

  要了解数据库索引的底层原理,咱们就得先了解一种叫树的数据结构,而树中很经典的一种数据结构就是二叉树!因此下面咱们就从二叉树到平衡二叉树,再到B-树,最后到B+树来一步一步了解数据库索引底层的原理!优化

二叉树(Binary Search Trees)

  二叉树是每一个结点最多有两个子树的树结构。一般子树被称做“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。二叉树有以下特性:

一、每一个结点都包含一个元素以及n个子树,这里0≤n≤2。
二、左子树和右子树是有顺序的,次序不能任意颠倒。左子树的值要小于父结点,右子树的值要大于父结点。

  光看概念有点枯燥,假设咱们如今有这样一组数[35 27 48 12 29 38 55],顺序的插入到一个数的结构中,步骤以下






  好了,这就是一棵二叉树啦!咱们能看到,经经过一系列的插入操做以后,本来无序的一组数已经变成一个有序的结构了,而且这个树知足了上面提到的两个二叉树的特性!

  可是若是一样是上面那一组数,咱们本身升序排列后再插入,也就是说按照[12 27 29 35 38 48 55]的顺序插入,会怎么样呢?

  因为是升序插入,新插入的数据老是比已存在的结点数据都要大,因此每次都会往结点的右边插入,最终致使这棵树严重偏科!!!上图就是最坏的状况,也就是一棵树退化为一个线性链表了,这样查找效率天然就低了,彻底没有发挥树的优点了呢!
为了较大发挥二叉树的查找效率,让二叉树再也不偏科,保持各科平衡,因此有了平衡二叉树!

平衡二叉树 (AVL Trees)

  平衡二叉树是一种特殊的二叉树,因此他也知足前面说到的二叉树的两个特性,同时还有一个特性:

它的左右两个子树的高度差的绝对值不超过1,而且左右两个子树都是一棵平衡二叉树。

  你们也看到了前面[35 27 48 12 29 38 55]插入完成后的图,其实就已是一颗平衡二叉树啦。

  那若是按照[12 27 29 35 38 48 55]的顺序插入一颗平衡二叉树,会怎么样呢?咱们看看插入以及平衡的过程:







  这棵树始终知足平衡二叉树的几个特性而保持平衡!这样咱们的树也不会退化为线性链表了!咱们须要查找一个数的时候就能沿着树根一直往下找,这样的查找效率和二分法查找是同样的呢!

  一颗平衡二叉树能容纳多少的结点呢?这跟树的高度是有关系的,假设树的高度为h,那每一层最多容纳的结点数量为2^(n-1),整棵树最多容纳节点数为2^0+2^1+2^2+...+2^(h-1)。这样计算,100w数据树的高度大概在20左右,那也就是说从有着100w条数据的平衡二叉树中找一个数据,最坏的状况下须要20次查找。若是是内存操做,效率也是很高的!可是咱们数据库中的数据基本都是放在磁盘中的,每读取一个二叉树的结点就是一次磁盘IO,这样咱们找一条数据若是要通过20次磁盘的IO?那性能就成了一个很大的问题了!那咱们是否是能够把这棵树压缩一下,让每一层可以容纳更多的节点呢?虽然我矮,可是我胖啊...

B-Tree

  这颗矮胖的树就是B-Tree,注意中间是杠精的杠而不是减,因此也不要读成B减Tree了~

  那B-Tree有哪些特性呢?一棵m阶的B-Tree有以下特性:

一、每一个结点最多m个子结点。
二、除了根结点和叶子结点外,每一个结点最少有m/2(向上取整)个子结点。
三、若是根结点不是叶子结点,那根结点至少包含两个子结点。
四、全部的叶子结点都位于同一层。
五、每一个结点都包含k个元素(关键字),这里m/2≤k<m,这里m/2向下取整。
六、每一个节点中的元素(关键字)从小到大排列。
七、每一个元素(关键字)字左结点的值,都小于或等于该元素(关键字)。右结点的值都大于或等于该元素(关键字)。

  是否是感受跟丈母娘张口问你要彩礼同样,列一堆的条件,并且每一条都让你很懵逼!下面咱们以一个[0,1,2,3,4,5,6,7]的数组插入一颗3阶的B-Tree为例,将全部的条件都串起来,你就明白了!







  那么,你是否对B-Tree的几点特性都清晰了呢?在二叉树中,每一个结点只有一个元素。可是在B-Tree中,每一个结点均可能包含多个元素,而且非叶子结点在元素的左右都有指向子结点的指针。

  若是须要查找一个元素,那流程是怎么样的呢?咱们看下图,若是咱们要在下面的B-Tree中找到关键字24,那流程以下



  从这个流程咱们能看出,B-Tree的查询效率好像也并不比平衡二叉树高。可是查询所通过的结点数量要少不少,也就意味着要少不少次的磁盘IO,这对
性能的提高是很大的。

  前面对B-Tree操做的图咱们能看出来,元素就是相似一、二、3这样的数值,可是数据库的数据都是一条条的数据,若是某个数据库以B-Tree的数据结构存储数据,那数据怎么存放的呢?咱们看下一张图

  普通的B-Tree的结点中,元素就是一个个的数字。可是上图中,咱们把元素部分拆分红了key-data的形式,key就是数据的主键,data就是具体的数据。这样咱们在找一条数的时候,就沿着根结点往下找就ok了,效率是比较高的。

B+Tree

  B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构。B+Tree与B-Tree的结构很像,可是也有几个本身的特性:

一、全部的非叶子节点只存储关键字信息。
二、全部卫星数据(具体数据)都存在叶子结点中。
三、全部的叶子结点中包含了所有元素的信息。
四、全部叶子节点之间都有一个链指针。

  若是上面B-Tree的图变成B+Tree,那应该以下:

  你们仔细对比于B-Tree的图能发现什么不一样?
  一、非叶子结点上已经只有key信息了,知足上面第1点特性!
  二、全部叶子结点下面都有一个data区域,知足上面第2点特性!
  三、非叶子结点的数据在叶子结点上都能找到,如根结点的元素四、8在最底层的叶子结点上也能找到,知足上面第3点特性!
  四、注意图中叶子结点之间的箭头,知足知足上面第4点特性!

B-Tree or B+Tree?

  在讲这两种数据结构在数据库中的选择以前,咱们还须要了解的一个知识点是操做系统从磁盘读取数据到内存是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是须要什么取什么。即便只须要一个字节,磁盘也会从这个位置开始,顺序向后读取必定长度的数据放入内存。这样作的理论依据是计算机科学中著名的局部性原理: 当一个数据被用到时,其附近的数据也一般会立刻被使用。
  预读的长度通常为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操做系统每每将主存和磁盘存储区分割为连续的大小相等的块,每一个存储块称为一页(在许多操做系统中,页得大小一般为4k)。

  B-Tree和B+Tree该如何选择呢?都有哪些优劣呢?
  一、B-Tree由于非叶子结点也保存具体数据,因此在查找某个关键字的时候找到便可返回。而B+Tree全部的数据都在叶子结点,每次查找都获得叶子结点。因此在一样高度的B-Tree和B+Tree中,B-Tree查找某个关键字的效率更高。
  二、因为B+Tree全部的数据都在叶子结点,而且结点之间有指针链接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree只须要找到该关键字而后沿着链表遍历就能够了,而B-Tree还须要遍历该关键字结点的根结点去搜索。
  三、因为B-Tree的每一个结点(这里的结点能够理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每一个页的大小有限是有限的,因此同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样一样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。
  鉴于以上的比较,因此在经常使用的关系型数据库中,都是选择B+Tree的数据结构来存储数据!下面咱们以mysql的innodb存储引擎为例讲解,其余相似sqlserver、oracle的原理相似!

innodb引擎数据存储

  在InnoDB存储引擎中,也有页的概念,默认每一个页的大小为16K,也就是每次读取数据时都是读取4*4k的大小!假设咱们如今有一个用户表,咱们往里面写数据

  这里须要注意的一点是,在某个页内插入新行时,为了避免减小数据的移动,一般是插入到当前行的后面或者是已删除行留下来的空间,因此在某一个页内的数据并不是彻底有序的(后面页结构部分有细讲),可是为了为了数据访问顺序性,在每一个记录中都有一个指向下一条记录的指针,以此构成了一条单向有序链表,不过在这里为了方便演示我是按顺序排列的!

  因为数据还比较少,一个页就能容下,因此只有一个根结点,主键和数据也都是保存在根结点(左边的数字表明主键,右边名字、性别表明具体的数据)。假设咱们写入10条数据以后,Page1满了,再写入新的数据会怎么存放呢?咱们继续看下图

  有个叫“秦寿生”的朋友来了,可是Page1已经放不下数据了,这时候就须要进行页分裂,产生一个新的Page。在innodb中的流程是怎么样的呢?

一、产生新的Page2,而后将Page1的内容复制到Page2。
二、产生新的Page3,“秦寿生”的数据放入Page3。
三、原来的Page1依然做为根结点,可是变成了一个不存放数据只存放索引的页,而且有两个子结点Page二、Page3。

  这里有两个问题须要注意的是
  一、为何要复制Page1为Page2而不是建立一个新的页做为根结点,这样就少了一步复制的开销了?
  若是是从新建立根结点,那根结点存储的物理地址可能常常会变,不利于查找。而且在innodb中根结点是会预读到内存中的,因此结点的物理地址固定会比较好!

  二、原来Page1有10条数据,在插入第11条数据的时候进行裂变,根据前面对B-Tree、B+Tree特性的了解,那这至少是一颗11阶的树,裂变以后每一个结点的元素至少为11/2=5个,那是否是应该页裂变以后主键1-5的数据仍是在原来的页,主键6-11的数据会放到新的页,根结点存放主键6?
  若是是这样的话新的页空间利用率只有50%,而且会致使更为频繁的页分裂。因此innodb对这一点作了优化,新的数据放入新建立的页,不移动原有页面的任何记录。

  随着数据的不断写入,这棵树也逐渐枝繁叶茂,以下图

  每次新增数据,都是将一个页写满,而后新建立一个页继续写,这里实际上是有个隐含条件的,那就是主键自增!主键自增写入时新插入的数据不会影响到原有页,插入效率高!且页的利用率高!可是若是主键是无序的或者随机的,那每次的插入可能会致使原有页频繁的分裂,影响插入效率!下降页的利用率!这也是为何在innodb中建议设置主键自增的缘由!

  这棵树的非叶子结点上存的都是主键,那若是一个表没有主键会怎么样?在innodb中,若是一个表没有主键,那默认会找建了惟一索引的列,若是也没有,则会生成一个隐形的字段做为主键!

  有数据插入那就有删除,若是这个用户表频繁的插入和删除,那会致使数据页产生碎片,页的空间利用率低,还会致使树变的“虚高”,下降查询效率!这能够经过索引重建来消除碎片提升查询效率!

innodb引擎数据查找

  数据插入了怎么查找呢?

一、找到数据所在的页。这个查找过程就跟前面说到的B+Tree的搜索过程是同样的,从根结点开始查找一直到叶子结点。
二、在页内找具体的数据。读取第1步找到的叶子结点数据到内存中,而后经过分块查找的方法找到具体的数据。

  这跟咱们在新华字典中找某个汉字是同样的,先经过字典的索引定位到该汉字拼音所在的页,而后到指定的页找到具体的汉字。innodb中定位到页后用了哪一种策略快速查找某个主键呢?这咱们就须要从页结构开始了解。

  左边蓝色区域称为Page Directory,这块区域由多个slot组成,是一个稀疏索引结构,即一个槽中可能属于多个记录,最少属于4条记录,最多属于8条记录。槽内的数据是有序存放的,因此当咱们寻找一条数据的时候能够先在槽中经过二分法查找到一个大体的位置。

  右边区域为数据区域,每个数据页中都包含多条行数据。注意看图中最上面和最下面的两条特殊的行记录Infimum和Supremum,这是两个虚拟的行记录。在没有其余用户数据的时候Infimum的下一条记录的指针指向Supremum,当有用户数据的时候,Infimum的下一条记录的指针指向当前页中最小的用户记录,当前页中最大的用户记录的下一条记录的指针指向Supremum,至此整个页内的全部行记录造成一个单向链表。

  行记录被Page Directory逻辑的分红了多个块,块与块之间是有序的,也就是说“4”这个槽指向的数据块内最大的行记录的主键都要比“8”这个槽指向的数据块内最小的行记录的主键要小。可是块内部的行记录不必定有序。

  每一个行记录的都有一个n_owned的区域(图中粉红色区域),n_owned标识这个这个块有多少条数据,伪记录Infimum的n_owned值老是1,记录Supremum的n_owned的取值范围为[1,8],其余用户记录n_owned的取值范围[4,8],而且只有每一个块中最大的那条记录的n_owned才会有值,其余的用户记录的n_owned为0。

  因此当咱们要找主键为6的记录时,先经过二分法稀疏索引中找到对应的槽,也就是Page Directory中“8”这个槽,“8”这个槽指向的是该数据块中最大的记录,而数据是单向链表结构因此没法逆向查找,因此须要找到上一个槽即“4”这个槽,而后经过“4”这个槽中最大的用户记录的指针沿着链表顺序查找到目标记录。

汇集索引&非汇集索引

  前面关于数据存储的都是演示的汇集索引的实现,若是上面的用户表须要以“用户名字”创建一个非汇集索引,是怎么实现的呢?咱们看下图:

  非汇集索引的存储结构与前面是同样的,不一样的是在叶子结点的数据部分存的再也不是具体的数据,而数据的汇集索引的key。因此经过非汇集索引查找的过程是先找到该索引key对应的汇集索引的key,而后再拿汇集索引的key到主键索引树上查找对应的数据,这个过程称为回表

  图中的这些名字均来源于网络,但愿没有误伤正在看这篇文章的你~^_^

innodb与MyISAM两种存储引擎对比

  上面包括存储和搜索都是拿的innodb引擎为例,那MyISAM与innodb在存储上有啥不一样呢?憋缩话,看图:

  上图为MyISAM主键索引的存储结构,咱们能看到的不一样是

一、主键索引树的叶子结点的数据区域没有存放实际的数据,存放的是数据记录的地址。
二、数据的存储不是按主键顺序存放的,按写入的顺序存放。

  也就是说innodb引擎数据在物理上是按主键顺序存放,而MyISAM引擎数据在物理上按插入的顺序存放。而且MyISAM的叶子结点不存放数据,因此非汇集索引的存储结构与汇集索引相似,在使用非汇集索引查找数据的时候经过非汇集索引树就能直接找到数据的地址了,不须要回表,这比innodb的搜索效率会更高呢!

索引优化建议?

  你们常常会在不少的文章或书中能看到一些索引的使用建议,好比说

一、like的模糊查询以%开头,会致使索引失效。
二、一个表建的索引尽可能不要超过5个。
三、尽可能使用覆盖索引。
四、尽可能不要在重复数据多的列上建索引。
五、。。。。。。。。。。。
六、。。。。。。。。。。。

  不少这里就不一一列举了!那看完这篇文章,咱们可否带着疑问去分析一下为何要有这些建议?为何like的模糊查询以%开头,会致使索引失效?为何一个表建的索引尽可能不要超过5个?为何? 为何??为何???相信看到这里的你再加上本身的一些思考应该有答案了吧?

  最后,推荐极客时间上的一门不错的mysql课程,学完对了解mysql底层原理有很大的帮助!
    

相关文章
相关标签/搜索