1.什么是B-Tree 和 B+Tree,他们是作什么用的?数据库
B-Tree是为了磁盘或其它存储设备而设计的一种多叉平衡查找树,B-Tree 和 B+Tree 普遍应用于文件存储系统以及数据库系统中。数组
一、每一个节点最多拥有m个子树数据结构
二、根节点至少有2个子树性能
三、分支节点至少拥有m/2颗子树(除根节点和叶子节点外都是分支节点)优化
四、全部叶子节点都在同一层spa
五、每一个节点最多能够有m-1个key设计
六、每一个节点中的key以升序排列指针
七、节点中key元素左节点的全部值都小于或等于该元素,元素右节点的全部值都大于或等于该元素blog
2.2.B-Tree的创建过程索引
下面咱们以一个[0,1,2,3,4,5,6,7]的数组插入一棵 3 阶的 B-Tree 为例,将全部的条件都串起来!
那么,你是否对 B-Tree 的几点特性都清晰了呢?在二叉树中,每一个结点只有一个元素。
可是在 B-Tree 中,每一个结点均可能包含多个元素,而且非叶子结点在元素的左右都有指向子结点的指针。
在 B-Tree 中,每一个结点均可能包含多个元素,而且非叶子结点在元素的左右都有指向子结点的指针。
2.3.B-Tree搜索原理
若是须要查找一个元素,那流程是怎么样的呢?咱们看下图,若是咱们要在下面的 B-Tree 中找到关键字 24,那流程以下:
从这个流程咱们能看出,B-Tree 的查询效率好像也并不比平衡二叉树高。可是查询所通过的结点数量要少不少,也就意味着要少不少次的磁盘 IO,这对性能的提高是很大的。
从前面对 B-Tree 操做的图,咱们能看出来,元素就是相似 一、二、3 这样的数值。
2.4.B-Tree在数据库中的应用
可是数据库的数据都是一条条的数据,若是某个数据库以 B-Tree 的数据结构存储数据,那数据怎么存放的呢?
普通的 B-Tree 的结点中,元素就是一个个的数字。可是上图中,咱们把元素部分拆分红了 key-data 的形式,Key 就是数据的主键,Data 就是具体的数据。
这样咱们在找一条数的时候,就沿着根结点往下找就 OK 了,效率是比较高的。
3.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 和 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 存储引擎为例讲解,其余相似 SQL Server、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 个?