数据库索引底层数据结构与算法(my sql)

 

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

二叉树(Binary Search Trees)

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

1、每个结点都包含一个元素以及n个子树,这里0≤n≤2。

2、左子树和右子树是有顺序的,次序不能任意颠倒。左子树的值要小于父结点,右子树的值要大于父结点。

光看概念有点枯燥,假设我们现在有这样一组数[35 28 49 13 30 37 60],顺序的插入到一个数的结构中,步骤如下

图1

图2

图3

图4

图3是错误示范,请大家在分析二叉树的时候不要犯这个错误。

 好了,这就是一棵二叉树啦!我们能看到,经通过一系列的插入操作之后,原本无序的一组数已经变成一个有序的结构了,并且这个树满足了上面提到的两个二叉树的特性!

但是如果同样是上面那一组数,我们自己升序排列后再插入,会怎么样呢?


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

平衡二叉树 (AVL Trees)

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

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

  大家也看到了前面[35 28 49 13 30 37 60]插入完成后的图,其实就已经是一颗平衡二叉树啦。

  那如果按照[35 28 49 13 30 37 60]的顺序插入一颗平衡二叉树,会怎么样呢?我们看看插入以及平衡的过程:

整个插入的以及平衡的过程就如上图所示。右子树与左子树的高度差绝对值大于1就需要调整。

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

  一颗平衡二叉树能容纳多少的结点呢?这跟树的高度是有关系的,假设树的高度为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有如下特性:

1、每个结点最多m个子结点。

2、除了根结点和叶子结点外,每个结点最少有m/2(向上取整)个子结点。

3、如果根结点不是叶子结点,那根结点至少包含两个子结点。

4、所有的叶子结点都位于同一层。

5、每个结点都包含k个元素(关键字),这里m/2≤k

7、每个元素(关键字)字左结点的值,都小于或等于该元素(关键字)。右结点的值都大于或等于该元素(关键字)。

是不是感觉很懵逼!下面我们以一个[0,1,2,3,4,5,6,7]的数组插入一颗3阶的B-Tree为例,将所有的条件都串起来,你就明白了!





不管裂变前后,所有叶子结点都在同一层,满足第4点特性,并且子叶结点的数量也满足第2点特性。

在二叉树中,每个结点只有一个元素。但是在B-Tree中,每个结点都可能包含多个元素,并且非叶子结点在元素的左右都有指向子结点的指针

 如果需要在B-Tree查找一个元素,那流程是怎么样的呢?我们看下图,如果我们要在下面的B-Tree中找到关键字24,那流程如下:

16<24<26所以数据在16和26中间找,然后就找到了24

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

  前面对B-Tree操作的图我们能看出来,元素就是类似1、2、3这样的数值,但是数据库的数据都是一条条的数据,如果某个数据库以B-Tree的数据结构存储数据,那数据怎么存放的呢?我们看下一张图


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

B+Tree

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

1、所有的非叶子节点只存储关键字信息。

2、所有卫星数据(具体数据)都存在叶子结点中。

3、所有的叶子结点中包含了全部元素的信息。

4、所有叶子节点之间都有一个链指针。

  如果上面B-Tree的图变成B+Tree,那应该如下:

注意红色标记框2中的箭头是双向的,懒得改了,声明一下。请注意一下的该截图中标记的1、2两处。

 b+图1


大家仔细对比于B-Tree的图能发现什么不同?

1、非叶子结点上已经只有key信息了,满足上面第1点特性!

2、所有叶子结点下面都有一个data区域,满足上面第2点特性!

3、非叶子结点的数据在叶子结点上都能找到,如根结点的元素4、8在最底层的叶子结点上也能找到,满足上面第3点特性!

4、注意图中叶子结点之间的箭头,满足满足上面第4点特性!

以上图为例,我们来讲解一下这颗B+Tree。Mysql的底层规定一个节点是16kB,在节点的每个元素存储的值是这样的【索引(比如数字15)+指向下一个子结点的指针(磁盘文件指针)(15后面的空白)】,在B+Tree中只在叶子节点存储数据,其他子结点存储的都是索引以及指针,这样的话在有限的节点里面,存储的索引就大大的增加了,树的阶数可以变得更小了,在数据量在千万级的情况下,对数据的检索时间将大大的减少。下面我们来具体的说明一下。假设上图中的索引是8位的整型(8B),我们的指针占的位置为(4B),那我们一个元素所占的内存空间就12B,一个节点我们可以放多少索引呢?是((16KB*1024B/KB)/12B=1365),一个节点可以存放1365个索引(元素)。假设我们叶子结点存储的(data+索引)为1K,那么我们上图中的3阶树可以存放多少数据呢?我们来计算一下1365*1365*16=29811600,可以存放接近3千万的数据,是不是觉得存储量很大,效率很高。同样的索引来查找,用B+tree和B-Tree有啥差别?我们来看一下,B-Tree的前面讲过了不讲了。现在我们根据索引来查找30,同样的从根结点开始,15<30<56在根节点的左子树上,20<30<49在这之间,继续往下找,然后你就找到了30 下的一条条数据。注意,在查找的时候我们是不是都进行了比较,B+Tree只是比较索引,但是B-Tree是带着数据的索引进行比较,速度谁更快不用讲。

在看B+Tree的时候“b+图1”中的红色标记1,大家都注意到了20的data在20的右子树的叶子结点上,这个称之为冗余结点,为什么要这么设计呢,这就引出另外一个问题,那就是图中的红色标记2,双向箭头。其实叶子结点 除了存储指针还存储了前后叶子结点的链针,这个链针和冗余结点的好处就是方便我们的范围查找,同时检索效率也大大的提高了。

B-Tree or B+Tree?

在讲这两种数据结构在数据库中的选择之前,我们还需要了解的一个知识点是操作系统从磁盘读取数据到内存是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。

预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k)。

B-Tree和B+Tree该如何选择呢?都有哪些优劣呢?

1、B-Tree因为非叶子结点也保存具体数据,所以在查找某个关键字的时候找到即可返回。而B+Tree所有的数据都在叶子结点,每次查找都得到叶子结点。所以在同样高度的B-Tree和B+Tree中,B-Tree查找某个关键字的效率更高。

2、由于B+Tree所有的数据都在叶子结点,并且结点之间有指针连接,在找大于某个关键字或者小于某个关键字的数据的时候,B+Tree只需要找到该关键字然后沿着链表遍历就可以了,而B-Tree还需要遍历该关键字结点的根结点去搜索。

3、由于B-Tree的每个结点(这里的结点可以理解为一个数据页)都存储主键+实际数据,而B+Tree非叶子结点只存储关键字信息,而每个页的大小有限是有限的,所以同一页能存储的B-Tree的数据会比B+Tree存储的更少。这样同样总量的数据,B-Tree的深度会更大,增大查询时的磁盘I/O次数,进而影响查询效率。

鉴于以上的比较,所以在常用的关系型数据库中,都是选择B+Tree的数据结构来存储数据!下面我们以mysql的innodb存储引擎为例讲解,其他类似sqlserver、oracle的原理类似!

延伸拓展:

Hash数据结构存储

  在mysql中建立索引的时候,可以选择是用hash,还是B+Tree.Hash是这样的,当你存入索引的时候会通过hash算法生成一个hashCode,在存入内存的时候就将你的这个hashCode和地址指针配对存入。这样做的好处就是当你在查询的时候,能够快速的定位,根本不需要去比较查找,只要根据这个值通过hash算法就可以找到你要的数据了。但是这种结构在实际应用中基本上是很少用的(至少我是没遇到过),为什么呢?因为在实际的应用中我们的sql语句查询用的比较多的是范围查找而不是精准定位。

综上所述,B+Tree成为了mysql,oracle等数据库的底层数据结构的不二之选。

索引的底层数据结构和算法


聚集索引&非聚集索引


  知道数据库基本的数据结构以后,下面我们来了解索引的底层数据结构和算法,用mySql为例。

MySql 有两种索引引擎:MyISAM索引实现(非聚集),innodb索引实现(聚集索引);存储引擎是形容数据的表的。

MyISAM索引实现(非聚集)

   这种索引下面会有三个文档,分别是:

MyISAM存储引擎是形容数据的表的:

xxx_MyISAM.frm :存储表定义结构

xxx_MyISAM.myd :存储表数据

xxx_MyISAM.myi :存储表的索引

从以上三个文件结合B+Tree的知识,大家就会很清楚的知道,MyISAM索引将表结构、索引、数据是分开在三个文件中进行存储的。

以下图为例,我们来进行讲解:


从上图中红色方框标记可以看到MyISAM的叶子结点中除了索引以外,存储的的不是data而是地址指针,在找数据的时候是通过地址指针去寻找的数据。除此之外索引的也是分开存储的(红色圆形标记),所以说MyISAM是非聚集的。

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

innodb引擎数据存储(聚集)

Innodb引擎下面有以下两种文件:

xxx.frm: 存储表定义结构

xxx.ibd:存储表数据和表的索引

它的聚集结构我们来看一下下图:

图innoDB-1

从上图中我们可以看到InnoDB的存储跟那两个文件是保持一致的,它的叶子结点存储的就是索引+完整的数据。

如图图innoDB-1所示,如果我们插入一个28,那么这个数应该插入在20与30之间,这样的话相应的(20,30)前后指针就需要变化,如果我们再插入一个29的话我们的B+TREE就需要分裂,一旦分裂的话,整颗树就要重构,因为指针地址变更。数据量大的时候树的重构是非常耗时的,所以innodb要求主键自增

在InnoDB存储引擎中,也有页的概念,默认每个页的大小为16K,也就是每次读取数据时都是读取4*4k的大小!假设我们现在有一个用户表,我们往里面写数据。这里需要注意的一点是,在某个页内插入新行时,为了不减少数据的移动,通常是插入到当前行的后面或者是已删除行留下来的空间,所以在某一个页内的数据并不是完全有序的,但是为了数据访问顺序性,在每个记录中都有一个指向下一条记录的指针,以此构成了一条单向有序链表。

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

有数据插入那就有删除,如果这个用户表频繁的插入和删除,那会导致数据页产生碎片,页的空间利用率低,还会导致树变的“虚高”,降低查询效率!这可以通过索引重建来消除碎片提高查询效率!

主键自增写入时新插入的数据不会影响到原有页,插入效率高!且页的利用率高!但是如果主键是无序的或者随机的,那每次的插入可能会导致原有页频繁的分裂,影响插入效率!降低页的利用率!这也是为什么在innodb中建议设置主键自增的原因!


innodb引擎数据查找

  数据插入了怎么查找呢?

1、找到数据所在的页。这个查找过程就跟前面说到的B+Tree的搜索过程是一样的,从根结点开始查找一直到叶子结点。

2、在页内找具体的数据。读取第1步找到的叶子结点数据到内存中,然后通过分块查找的方法找到具体的数据。

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

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

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

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

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

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

InnoDB的数据引擎同时要求主键尽量是整型,为什么?我们下面来分析一下

  假设我们的主键是英文字符串,那么innodb数据引擎在进行b+Tree的构建的时候就会将英文字母翻译成数字,比如as,(假如:a=65,s=83)那么innodb会将其翻译成6583,6583就是主键,中间的一次翻译就会导致效率的降低。所以InnoDB数据引擎要求主键尽量是整型。

  当存在多个索引的时候具体的聚集结构是什么样的呢,如下图所示:

当你又多个索引的时候就将你的索引排序就可以了。


innodb与MyISAM两种存储引擎对比

  那MyISAM与innodb在存储上有啥不同呢,根据以上的图我们能看到的不同是

1、MyISAM主键索引树的叶子结点的数据区域没有存放实际的数据,存放的是数据记录的地址。

2、MyISAM数据的存储不是按主键顺序存放的,按写入的顺序存放。

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

以上是个人的理解,仅供参考,望与各位码农共勉,不喜勿喷,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容