mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

摘要:

        本篇介绍下Mysql的InnoDB索引相关知识,从各种树到索引原理到存储的细节。

        InnoDB是Mysql的默认存储引擎(Mysql5.5.5之前是MyISAM,文档)。本着高效学习的目的,本篇以介绍InnoDB为主。少量涉及MyISAM作为对照。

这篇文章是我在学习过程中总结完毕的。内容主要来自书本和博客(參考文献会给出)。过程中增加了一些自己的理解。描写叙述不准确的地方烦请指出。

1 各种树形结构

        本来不打算从二叉搜索树開始,由于网上已经有太多相关文章,可是考虑到清晰的图示对理解问题有非常大帮助。也为了保证文章完整性,最后还是加上了这部分。

        先看看几种树形结构:

        1 搜索二叉树:每一个节点有两个子节点。数据量的增大必定导致高度的高速添加。显然这个不适合作为大量数据存储的基础结构。

        2 B树:一棵m阶B树是一棵平衡的m路搜索树。

最重要的性质是每一个非根节点所包括的keyword个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;一个节点的子节点数量会比keyword个数多1,这样keyword就变成了子节点的切割标志。

通常会在图示中把keyword画到子节点中间。很形象,也easy和后面的B+树区分。因为数据同一时候存在于叶子节点和非叶子结点中,无法简单完毕按顺序遍历B树中的keyword,必须用中序遍历的方法。

        3 B+树:一棵m阶B树是一棵平衡的m路搜索树。

最重要的性质是每一个非根节点所包括的keyword个数 j 满足:┌m/2┐ - 1 <= j <= m;子树的个数最多能够与keyword一样多。

非叶节点存储的是子树里最小的keyword。同一时候数据节点仅仅存在于叶子节点中。且叶子节点间添加了横向的指针,这样顺序遍历全部数据将变得很easy。

        4 B*树:一棵m阶B树是一棵平衡的m路搜索树。最重要的两个性质是1每一个非根节点所包括的keyword个数 j 满足:┌m2/3┐ - 1 <= j <= m;2非叶节点间加入了横向指针。






        B/B+/B*三种树有相似的操作,比方检索/插入/删除节点。这里仅仅重点关注插入节点的情况,且仅仅分析他们在当前节点已满情况下的插入操作。由于这个动作略微复杂且能充分体现几种树的差异。

与之对照的是检索节点比較easy实现,而删除节点仅仅要完毕与插入相反的过程就可以(在实际应用中删除并非插入的全然逆操作。往往仅仅删除数据而保留下空间为兴许使用)。

        先看B树的分裂,下图的红色值即为每次新插入的节点。

每当一个节点满后,就须要发生分裂(分裂是一个递归过程,參考以下7的插入导致了两层分裂)。因为B树的非叶子节点相同保存了键值。所以已满节点分裂后的值将分布在三个地方:1原节点,2原节点的父节点。3原节点的新建兄弟节点(參考5。7的插入过程)。

分裂有可能导致树的高度添加(參考3。7的插入过程)。也可能不影响树的高度(參考5。6的插入过程)。


        B+树的分裂:当一个结点满时。分配一个新的结点,并将原结点中1/2的数据拷贝到新结点。最后在父结点中添加新结点的指针;B+树的分裂仅仅影响原结点和父结点,而不会影响兄弟结点。所以它不须要指向兄弟节点的指针。



        B*树的分裂:当一个结点满时,假设它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中。再在原结点插入keyword,最后改动父结点中兄弟结点的keyword(由于兄弟结点的keyword范围改变了)。假设兄弟也满了。则在原结点与兄弟结点之间添加新结点。并各复制1/3的数据到新结点。最后在父结点添加新结点的指针。

能够看到B*树的分裂很巧妙。由于B*树要保证分裂后的节点还要2/3满,假设採用B+树的方法,仅仅是简单的将已满的节点一分为二,会导致每一个节点仅仅有1/2满,这不满足B*树的要求了。所以B*树採取的策略是在本节点满后。继续插入兄弟节点(这也是为什么B*树须要在非叶子节点加一个兄弟间的链表),直到把兄弟节点也塞满,然后拉上兄弟节点一起凑份子,自己和兄弟节点各出资1/3成立新节点,这种结果是3个节点刚好是2/3满,达到B*树的要求,皆大欢喜。


        B+树适合作为数据库的基础结构。全然是由于计算机的内存-机械硬盘两层存储结构。内存能够完毕高速的随机訪问(随机訪问即给出随意一个地址,要求返回这个地址存储的数据)可是容量较小。而硬盘的随机訪问要经过机械动作(1磁头移动 2盘片转动),訪问效率比内存低几个数量级,可是硬盘容量较大。典型的数据库容量大大超过可用内存大小,这就决定了在B+树中检索一条数据非常可能要借助几次磁盘IO操作来完毕。

例如以下图所看到的:通常向下读取一个节点的动作可能会是一次磁盘IO操作,只是非叶节点一般会在初始阶段加载内存以加快訪问速度。同一时候为提高在节点间横向遍历速度。真实数据库中可能会将图中蓝色的CPU计算/内存读取优化成二叉搜索树(InnoDB中的page directory机制)。


        真实数据库中的B+树应该是很扁平的,能够通过向表中顺序插入足够数据的方式来验证InnoDB中的B+树究竟有多扁平。我们通过例如以下图的CREATE语句建立一个仅仅有简单字段的測试表,然后不断加入数据来填充这个表。通过下图的统计数据(来源见參考文献1)能够分析出几个直观的结论,这几个结论宏观的展现了数据库里B+树的尺度。

        1 每一个叶子节点存储了468行数据,每一个非叶子节点存储了大约1200个键值,这是一棵平衡的1200路搜索树!

        2 对于一个22.1G容量的表。也仅仅须要高度为3的B+树就能存储了,这个容量大概能满足非常多应用的须要了。假设把高度增大到4。则B+树的存储容量立马增大到25.9T之巨!

        3 对于一个22.1G容量的表,B+树的高度是3。如果要把非叶节点所有载入到内存也仅仅须要少于18.8M的内存(怎样得出的这个结论?由于对于高度为2的树,1203个叶子节点也仅仅须要18.8M空间,而22.1G从良表的高度是3,非叶节点1204个。

同一时候我们如果叶子节点的尺寸是大于非叶节点的,由于叶子节点存储了行数据而非叶节点仅仅有键和少量数据。

)。仅仅使用如此少的内存就能够保证仅仅须要一次磁盘IO操作就检索出所需的数据,效率是很之高的。



2 Mysql的存储引擎和索引

        能够说数据库必须有索引,没有索引则检索过程变成了顺序查找,O(n)的时间复杂度差点儿是不能忍受的。我们很easy想象出一个仅仅有单keyword组成的表怎样使用B+树进行索引,仅仅要将keyword存储到树的节点就可以。当数据库一条记录里包括多个字段时,一棵B+树就仅仅能存储主键。假设检索的是非主键字段,则主键索引失去作用,又变成顺序查找了。

这时应该在第二个要检索的列上建立第二套索引。

 这个索引由独立的B+树来组织。

有两种常见的方法能够解决多个B+树訪问同一套表数据的问题。一种叫做聚簇索引(clustered index ),一种叫做非聚簇索引(secondary index)。

这两个名字尽管都叫做索引。但这并非一种单独的索引类型。而是一种数据存储方式。对于聚簇索引存储来说,行数据和主键B+树存储在一起。辅助键B+树仅仅存储辅助键和主键。主键和非主键B+树差点儿是两种类型的树。

对于非聚簇索引存储来说。主键B+树在叶子节点存储指向真正数据行的指针,而非主键。

        InnoDB使用的是聚簇索引,将主键组织到一棵B+树中,而行数据就储存在叶子节点上,若使用"where id = 14"这种条件查找主键,则依照B+树的检索算法就可以查找到相应的叶节点,之后获得行数据。

若对Name列进行条件搜索。则须要两个步骤:第一步在辅助索引B+树中检索Name。到达其叶子节点获取相应的主键。第二步使用主键在主索引B+树种再运行一次B+树检索操作,终于到达叶子节点就可以获取整行数据。

        MyISM使用的是非聚簇索引,非聚簇索引的两棵B+树看上去没什么不同。节点的结构全然一致仅仅是存储的内容不同而已。主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方,这两颗B+树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有不论什么区别。因为索引树是独立的。通过辅助键检索无需訪问主键的索引树。

        为了更形象说明这两种索引的差别。我们假想一个表例如以下图存储了4行数据。当中Id作为主索引。Name作为辅助索引。图示清晰的显示了聚簇索引和非聚簇索引的差异。




        我们重点关注聚簇索引,看上去聚簇索引的效率明显要低于非聚簇索引。由于每次使用辅助索引检索都要经过两次B+树查找,这不是多此一举吗?聚簇索引的优势在哪?

        1 因为行数据和叶子节点存储在一起,这样主键和行数据是一起被加载内存的,找到叶子节点就能够立马将行数据返回了。假设依照主键Id来组织数据,获得数据更快。

        2 辅助索引使用主键作为"指针" 而不是使用地址值作为指针的优点是,降低了当出现行移动或者数据页分裂时辅助索引的维护工作,使用主键值当作指针会让辅助索引占用很多其它的空间,换来的优点是InnoDB在移动行时无须更新辅助索引中的这个"指针"。

也就是说行的位置(实现中通过16K的Page来定位,后面会涉及)会随着数据库里数据的改动而发生变化(前面的B+树节点分裂以及Page的分裂),使用聚簇索引就能够保证无论这个主键B+树的节点怎样变化,辅助索引树都不受影响。

3 Page结构

        假设说前面的内容偏向于解释原理,那后面就開始涉及详细实现了。

        理解InnoDB的实现不得不提Page结构,Page是整个InnoDB存储的最基本构件,也是InnoDB磁盘管理的最小单位。与数据库相关的全部内容都存储在这样的Page结构里。

Page分为几种类型,常见的页类型有数据页(B-tree Node)Undo页(Undo Log Page)系统页(System Page) 事务数据页(Transaction System Page)等。单个Page的大小是16K(编译宏UNIV_PAGE_SIZE控制),每一个Page使用一个32位的int值来唯一标识,这也正好相应InnoDB最大64TB的存储容量(16Kib * 2^32 = 64Tib)。一个Page的基本结构例如以下图所看到的:


        每一个Page都有通用的头和尾。可是中部的内容依据Page的类型不同而发生变化。Page的头部里有我们关心的一些数据,下图把Page的头部具体信息显示出来:


        我们重点关注和数据组织结构相关的字段:Page的头部保存了两个指针,分别指向前一个Page和后一个Page,头部还有Page的类型信息和用来唯一标识Page的编号。依据这两个指针我们非常easy想象出Page链接起来就是一个双向链表的结构。

        再看看Page的主体内容。我们主要关注行数据和索引的存储,他们都位于Page的User Records部分,User Records占领Page的大部分空间,User Records由一条一条的Record组成,每条记录代表索引树上的一个节点(非叶子节点和叶子节点)。

在一个Page内部。单链表的头尾由固定内容的两条记录来表示。字符串形式的"Infimum"代表开头,"Supremum"代表结尾。

这两个用来代表开头结尾的Record存储在System Records的段里。这个System Records和User Records是两个平行的段。InnoDB存在4种不同的Record,它们各自是1主键索引树非叶节点 2主键索引树叶子节点 3辅助键索引树非叶节点 4辅助键索引树叶子节点。

这4种节点的Record格式有一些差异,可是它们都存储着Next指针指向下一个Record。兴许我们会具体介绍这4种节点,如今仅仅须要把Record当成一个存储了数据同一时候含有Next指针的单链表节点就可以。


        User Record在Page内以单链表的形式存在。最初数据是依照插入的先后顺序排列的,可是随着新数据的插入和旧数据的删除,数据物理顺序会变得混乱。但他们依旧保持着逻辑上的先后顺序。


        把User Record的组织形式和若干Page组合起来,就看到了略微完整的形式。


        如今看下怎样定位一个Record:

        1 通过根节点開始遍历一个索引的B+树,通过各层非叶子节点终于到达一个Page,这个Page里存放的都是叶子节点。

        2 在Page内从"Infimum"节点開始遍历单链表(这样的遍历往往会被优化),假设找到该键则成功返回。假设记录到达了"supremum"。说明当前Page里没有合适的键。这时要借助Page的Next Page指针,跳转到下一个Page继续从"Infimum"開始逐个查找。

具体看下不同类型的Record里究竟存储了什么数据,依据B+树节点的不同,User Record能够被分成四种格式。下图种依照颜色予以区分。

1 主索引树非叶节点(绿色)

        1 子节点存储的主键里最小的值(Min Cluster Key on Child),这是B+树必须的,作用是在一个Page里定位到详细的记录的位置。

        2 最小的值所在的Page的编号(Child Page Number),作用是定位Record。

2 主索引树叶子节点(黄色)

        1 主键(Cluster Key Fields),B+树必须的。也是数据行的一部分

        2 除去主键以外的全部列(Non-Key Fields)。这是数据行的除去主键的其它全部列的集合。

        这里的1和2两部分加起来就是一个完整的数据行。

3 辅助索引树非叶节点非(蓝色)

        1 子节点里存储的辅助键值里的最小的值(Min Secondary-Key on Child)。这是B+树必须的,作用是在一个Page里定位到详细的记录的位置。

        2 主键值(Cluster Key Fields)。非叶子节点为什么要存储主键呢?由于辅助索引是能够不唯一的,可是B+树要求键的值必须唯一。所以这里把辅助键的值和主键的值合并起来作为在B+树中的真正键值。保证了唯一性。可是这也导致在辅助索引B+树中非叶节点反而比叶子节点多了4个字节。(即下图中蓝色节点反而比红色多了4字节)

        3 最小的值所在的Page的编号(Child Page Number),作用是定位Record。

4 辅助索引树叶子节点(红色)

        1 辅助索引键值(Secondary Key Fields),这是B+树必须的。

        2 主键值(Cluster Key Fields),用来在主索引树里再做一次B+树检索来找到整条记录。


        以下是本篇最重要的部分了,结合B+树的结构和前面介绍的4种Record的内容,我们最终能够画出一幅全景图。因为辅助索引的B+树与主键索引有相似的结构,这里仅仅画出了主键索引树的结构图,仅仅包括了"主键非叶节点"和"主键叶子节点"两种节点。也就是上图的的绿色和黄色的部分。


        把上图还原成以下这个更简洁的树形示意图,这就是B+树的一部分。注意Page和B+树节点之间并没有一一相应的关系。Page仅仅是作为一个Record的保存容器,它存在的目的是便于对磁盘空间进行批量管理,上图中的编号为47的Page在树形结构上就被拆分成了两个独立节点。


        至此本篇就算结束了。本篇仅仅是对InnoDB索引相关的数据结构和实现进行了一些梳理总结,并未涉及到Mysql的实战经验。这主要是基于几点原因:

        1 原理是基石,仅仅有充分了解InnoDB索引的工作方式,我们才有能力高效的使用好它。

        2 原理性知识特别适合使用图示,我个人很喜欢这样的表达方式。

        3 关于InnoDB优化,在《高性能Mysql》里有更加全面的介绍,对优化Mysql感兴趣的同学全然能够自己获取相关知识,我自己的积累还未达到能分享这些内容的地步。

        另:对InnoDB实现有很多其它兴趣的同学能够看看Jeremy Cole的博客(參考文献三篇文章的来源)。这位老兄曾先后在Mysql。Yahoo,Twitter。Google从事数据库相关工作。他的文章很棒!

參考文献:

[1] Jeremy Cole The physical structure of InnoDB index pages

[2] Jeremy Cole B+Tree index structures in InnoDB

[3] Jeremy Cole The physical structure of records in InnoDB

[4] 姜承尧  MySQL技术内幕-InnoDB存储引擎 第二版

[5] Schwartz,B / Zaitsev,P / Tkach 高性能Mysql 第三版

[6] B-tree wiki

posted on 2017-05-29 21:48  mthoutai  阅读(325)  评论(0编辑  收藏  举报