赞
踩
作为一个软件开发工程师,大家对数据库肯定是再熟悉不过了。主流的数据存储系统,在业务开发中有着很重要的地位。在工作中常常为了加速数据库中数据的查找速度,常用的思路就是对表中数据创建索引。那么想没想过数据库索引是如何实现的呢?底层使用的是什么数据结构和算法呢???
如何定义清楚问题呢?除了对问题进行详细的调研之外还可以通过对一些模糊的需求进行假设,来限定要解决的问题的范围。
对数据库非常了解,针对这个问题能把索引的需求定义得非常清楚。但是大部分可能只是了解一小部分常用的SQL,因此假设要解决的问题只包含两个常用的需求:
select * from user where id=1234;
select * from user where id > 1234 and id < 2345;
除了这些功能性需求之外往往还会涉及一些其他的非功能性需求,如安全、性能、用户体验等等。本文对于非功能性需求着重考虑性能方面的需求。性能方面的需求主要考察时间和空间两个方面就是执行效率和存储空间。。。
在执行效率方面,希望通过索引,查询数据的效率尽可能的高;在存储方面希望索引不要消耗太多的内存空间。
知道了需求,那么能否利用已经学习过的数据结构去解决问题呢?支持快速查询、插入等操作的动态数据结构,学习过散列表、平衡二叉树、跳表。。。
为了让二叉查找树支持按照区间来查找数据,可以对它进行改造:
改造之后,如果我们要求某个区间的值。那么只需要拿区间的值在树中查找当查找到某个叶子节点之后,再顺着链表往后遍历,直到链表中的结点数据值大于区间的终止值为止。所有遍历到的数据就是符合区间值的所有数据。
但是如果要为很多的数据构建索引,如果索引存储在内存中,尽管内存访问的速度非常快,查询的效率非常高,但是占用的内存会非常多。。。
数据很多的情况下建立索引要消耗很多的内存空间,且对内存的需求是无法满足的。如何解决这个索引占用太多内存的问题呢???
可以借助时间换空间的思路,将索引存储在硬盘中并非内存中。硬盘比内存的读取速度要慢很多、将索引存在硬盘中尽管是减少了内存消耗,但是在查找过程中需要读取索引因此数据查询效率相应降低很多。。。
二叉查找树改造之后支持区间查找的功能就实现了。但是为了节省内存,如果将树存储到硬盘中,每个节点的读取(或访问)都对应一次IO操作。树的高度就等于每次查询数据时磁盘IO操作的次数。。。
因此我们优化的重点就是尽量减少磁盘IO操作,就是尽量降低树的高度,那么应该如何做呢???
如果把索引构建成m叉树高度是不是比二叉树要小呢?图所示给16个数据构建二叉树索引,树的高度是4查找一个数据需要4个磁盘IO操作(如果根节点存储在内存中其他节点存储在硬盘中),如果m叉树中的m是100,那对一亿个数据构建索引树的高度也只是3,最多只要3次磁盘IO就能获取到数据。磁盘IO变少了,查找数据的效率也就提高了。。。
如果将m叉树实现B+树索引,使用代码实现出来,就是下面这个样子(假设给int类型的数据库字段添加索引,所以代码中的keywords是int类型的):
public class BmoreTreeDemo { /** * 这是 B+树非叶子节点的定义 * * 假设keywords=[3,5,8,10] * 4个键值将数据分为5个区间:(-INF,3),[3,5),[5,8),[8,10),[10,INF) * 5个区间分别对应:children[0]...children[4] * * m值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小: * PAGE_SIZE = (m-1)*4[keywords大小]+m*8[children大小] */ public static int m=5;//5叉树 public int[] keywords = new int[m-1];//键值,用来划分数据区间 public BmoreTreeDemo[] children = new BmoreTreeDemo[m];//保存子节点指针 } /** * 这是B+树中叶子节点的定义 * * B+树中的叶子节点跟内部节点是不一样的, * 叶子节点存储的是值,而不是区间。 * 这个定义里,每个叶子节点存储3个数据行的键值及地址信息 * * k值是事先计算得到的,计算的依据是让所有信息的大小正好等于页的大小: * PAGE_SIZE = k*4[keywords]+k*8[dataAddress大小]+8[prev大小]+8[next大小] */ class BPlusTreeLeafNode{ public static int k =3; public int[] keywords = new int[k];//数据的键值 public long[] dataAddress = new long[k];//数据地址 public BPlusTreeLeafNode prev; //这个结点在链表中的前驱节点 public BPlusTreeLeafNode next; //这个结点在链表中的后继节点 }
对于相同个数的数据构建m叉树索引,m叉树中的m越大,树的高度就越小,那么m叉树中的m是不是越大越好呢?到底是多大才最合适呢???
不管是内存中的数据还是硬盘中的数据,操作系统都是按页来读取的,一次会读一页的数据。如果要读取的数据量超过一页的大小就会触发多次IO操作,在选择m大小的时候要尽量让每个节点大小等于一个页的大小。读取一个节点,只需要一次磁盘IO操作。。。
尽管索引可以提高数据库的查询效率,但是索引也是有弊端的,它会让写入数据的效率下降,那么这又是为什么呢???
主要的原因就是数据的写入过程会涉及到索引的更新。。。
对于一个B+树来说,m值是根据页的大小事先计算好的,就是说每个节点最多只能有m个子节点。在往数据库中写入数据的过程中,这样有可能使索引中某些节点的子节点个数超过m,这个节点大小超过了一个页的大小,读取这样一个节点就会导致多次磁盘IO操作,那么该如何解决这个问题呢???
实际上处理思路不是很复杂,只需要将这个节点分裂成两个节点。节点分裂之后其上层父节点子节点个数可能超过m个。可以将父节点分裂成两个节点,级联反应从下往下,一直影响到根节点。(下图中B+树是一个三叉树,限定叶子节点中,数据个数超过2个就分裂节点;非叶子节点,子节点个数超过3个就分裂节点)。
要时刻保证B+树索引是一个m叉树,因此索引的存在会导致数据库写入的速度降低,实际上不光写入数据会变慢,删除也会变慢,这是因为什么呢???
在删除某个数据的时候也要对应的更新索引节点,有点类似跳表中删除数据的思路。频繁的删除数据,会导致某些结点中,子节点个数变得非常少,很长时间以后如果每个节点的子节点都比较少,势必会影响索引的效率。。
可以设置一个阈值。B+树中这个阈值等于m/2。如果某个节点的子节点个数小于m/2,就将它跟相邻的兄弟节点合并。不过合并之后子节点个数可能会超过m。针对这种情况,可以借助插入数据时候的处理方法,再次分裂节点。。。
下面是一个删除操作的例子,可以对比下(图中的 B+ 树是一个五叉树。我们限定叶子节点中,数据的个数少于 2 个就合并节点;非叶子节点中,子节点的个数少于 3 个就合并节点。)
B+树的结构和操作跟跳表非常相似,理论上来讲对跳表稍加改造可以替代B+树,作为数据库的索引实现的。。
总结一下B+树的特点:
B树实际上是低级版的B+树,或者说B+树是B树的改进版。B树和B+树的不同点主要集中在这几个地方:
也就是说B树只是一个每个节点的子节点个数不能小于m/2的m叉树。。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。