原创性申明
引言
传统的数据库管理系统把所有数据都放在磁盘上进行管理,所以称作磁盘数据库(DRDB: Disk-Resident Database)。磁盘数据库需要频繁地访问磁盘来进行数据的操作,磁盘的读写速度远远小于CPU处理数据的速度,所以磁盘数据库的瓶颈出现在磁盘读写上。
基于此,内存数据库的概念被提出来了。内存数据库(MMDB:Main Memory Database,也叫主存数据库)[1],就是将数据全部或者大部分放在内存中进行操作的数据库管理系统,对查询处理、并发控制与恢复的算法和数据结构进行重新设计,以更有效地使用CPU周期和内存。相对于磁盘,内存的数据读写速度要高出几个数量级,将数据保存在内存中相比从磁盘上访问能够极大地提高应用的性能。
近十几年来,内存的发展一直遵循摩尔定律[2],内存的价格一直下降,而内存的容量一直在增加。现在的主流服务器,几百GB或者几TB的内存都很常见,内存的发展使得内存数据库得以实现。
由于内存数据库与传统的磁盘数据库在设计和架构上都大不相同,所以传统的数据库索引不适用于内存数据库。研究者为改进内存数据库的索引结构做了相当多的研究跟工作。其中,影响较大的索引有早期的T树、基于缓存敏感(cacheconscious)的CSS/CSB+树,Trie-tree和Hash等等。本文就这几种有代表性的索引算法进行研究和分析,为进一步改进内存数据库索引算法和提高索引性能打下坚实的基础。
2、T-tree
2.1 T-tree
T-tree是针对主存访问优化的索引技术[3]。T-tree是一种一个节点中包含多个索引条目的平衡二叉树,T-tree的索引项无论是从大小还是算法上都比B-tree精简得多。T-tree的搜索算法不分搜索的值在当前的节点还是在内存中的其他地方,每访问到一个新的索引节点,索引的范围减少一半。
图2-1T-Tree的结点
T-tree索引用来实现关键字的范围查询。T-tree是一棵特殊平衡的二叉树(AVL),它的每个节点存储了按键值排序的一组关键字。T-tree除了较高的节点空间占有率,遍历一棵树的查找算法在复杂程度和执行时间上也占有优势。现在T-tree己经成为内存数据库中最主要的一种索引方式。
T-tree具有以下特点:1)左子树与右子树之差不超过1,2)在一个存储节点可以保存多个键值,它的最左与最右键值分别为这个节点的最小与最大键值,它的左子树仅仅包含那些键值小于或等于最小键值的一记录,同理右子树只包括那些键值大于或等于最大键值的记录,3)同时拥有左右子树的节点被称为内部节点,只拥有一个子树的节点被称为半叶节点,没有子树的节点被称为叶子,4)为了保持空间的利用率,每一个内部节点都需要包含一个最小数目的键值。由此可知T-tree是一个每个结点含有多个关键字的平衡二叉树,每个节点内的关键字有序排列,左子树都要比根节点关键字小,右子树都要比根节点关键字大。
在上述T-tree结点结构中,包含如下信息:
(1)balance(平衡因子),其绝对值不大于1,balance=右子树高度-左子树高度;
(2)Left_child_ptr和Right_child_ptr分别表示当前结点的左子树和右子树指针;
(3)Max_Item表示结点中所能容纳的键值的最大数;
(4)Key[0]至K[Max_Item-1]为结点内存放的关键字;
(5)nItem是当前节点实际存储的关键字个数。
对于T-tree有如下特征:
(1)与AVL树相似,T-tree中任何结点的左右子树的高度之差最大为1;
(2)与AVL树不同,T-tree的结点中可存储多个键值,并且这些键值排列有序;
(3)T-tree结点的左子树中容纳的键值不大于该结点中的最左键值;右子树中容纳的键值不小于该结点中的最右键值;
(4)为了保证每个结点具有较高的空间占用率,每个内部结点所包含的键值数目必须不小于某个指定的值,通常为(Max_Item-2)(Max_Item为结点中最大键值目)。
2.2 T-tree索引的操作
用T-tree作为索引方式主要完成三个工作:查找,插入,删除。其中插入和删除都是以查找为基础。下面分别介绍三种操作的流程。
2.2.1 查找
T-tree的查找类似于二叉树,不同之处主要在于每一结点上的比较不是针对结点中的各个元素值,而是首先检查所要查找的目标键值是否包含在当前结点的最左键值和最右键值所确定的范围内,如果是的话,则在当前结点的键值列表中使用二分法进行查找;如果目标键值小于当前结点的最左键值,则类似地搜索当前结点的左孩子结点;如果目标键值大于当前结点的最右键值,则类似地搜索当前结点的右孩子结点。
2. 2.2 插入
T-tree的插入是以查找为基础,应用查找操作定位目标键值插入位置,并记下查找过程所遇到的最后结点。如果查找成功,判断此结点中是否有足够的存储空间。如果有,则将目标键值插入结点中;否则将目标键值插入此结点,然后将结点中的最左键值插入到它的左子树中(此时是递归插入操作),之后结束;否则分配新结点,并插入目标键值;然后根据目标键值与结点的最大最小键值之间的关系,将新分配的结点链接为结点的左孩子或右孩子;对树进行检查,判断T-tree的平衡因子是否满足条件,如果平衡因子不满足则执行旋转操作。
2.2.3 删除
T-tree的删除操作也是以查找为基础,应用查找操作定位目标键值。如果查找失败,则结束;否则令N为目标键值所在的结点,并从结点N中删除目标键值;删除节点后,如果结点N为空,则删除结点N,并对树的平衡因子进行检查,判断是否需要执行旋转操作;如果结点N中的键值数量少于最小值,则根据N的平衡因子决定从结点N的左子树中移出最大的键值或者右子树中移出最小值来填充。
2.3 T-tree索引实现关键技术
实现T-tree索引即要实现T-tree的查找,插入和删除。其中又以查找为基础,对T-tree的维护也就是T-tree的旋转为关键。当由于插入或删除键值导致树的失衡,则要进行T-tree的旋转。使之重新达到平衡。
在插入情况下,需要依次对所有沿着从新创建结点到根结点路径中的结点进行检查,直到出现如下两种情况之一时中止:某个被检查结点的两个子树高度相等,此时不需要执行旋转操作;某个被检查结点的两个子树的高度之差大于1,此时对该结点仅需执行一次旋转操作即可。
在删除情况下,类似地需要依次对所有沿着从待删除结点的父结点到根结点路径中的结点进行检查,在检查过程中当发现某个结点的左右子树高度之差越界时,需要执行一次旋转操作。与插入操作不同的是,执行完旋转操作之后,检查过程不能中止,而是必须一直执行到检查完根结点。
由此可以看出,对于插入操作,最多只需要一次旋转操作即可使T-tree恢复到平衡状态;而对于删除操作则可能会引起向上的连锁反应,使高层结点发生旋转,因而可能需要进行多次旋转操作。
为了对T-tree进行平衡,需要进行旋转操作,旋转是T-tree中最关键也是最难的的操作,下面介绍T-tree旋转的技术。旋转可分为四种情况:由左孩子的左子树的插入(或者删除)引起的旋转记为LL旋转,类似有LR,RR及RL旋转。插入时的情况与删除类似。
3、CSS/CSB+树
3.1 CSS-trees
3.1.1Introduction
CSS-trees(Cache-SensitiveSearch Trees),可以提供比二分查找更为迅速的查询操作而又不需大量额外的空间[4]。该技术在在一个以排好序的数组顶端存储一个目录结构,且该目录结构的节点大小与机器cache-line大小相匹配。将该目录结构存储在数组中而无需存储内部节点的指针,子节点可通过数组偏移量定位,这与B+-trees不同。
3.2 FULL CSS-Tree
构造一棵结点包含m个键值的查询树,树的深度是d,那么一直到d-1的深度这棵树是一棵完全(m+1)-查询树,而在d层叶子结点从左往右分布。一棵m=4的实例树图3-1所示,其中方块数就是结点数,且每个结点有四个键值。
CSS-Tree的结点可以存储在数组中,如图3-2所示:
3.2.1 构造FULL CSS-Tree
将一个已排好序的数组构造一棵相应的Full CSS-Tree,首先将数组分为两部分,并且在叶子节点和数组元素间建立匹配。然后从最后一个内部节点开始,将节点直接左子树的最大键值作为节点入口。对于某一些内部节点,也就是最深层最后一个叶子节点的祖先,可能完全键值,可以用数组前半部分最后的一个元素来填充这些键值,所以在某些内部节点会有一些复制的键值。尽管要增量更新一棵Full CSS-Tree树是很困难的,但构造这样一棵树花费并不大。实验表明对于有着两千五百万键值的数组,构造其相应的Full CSS-Tree花费的时间不足一秒。
3.2.2查询Full CSS-Tree
从根节点开始,每次都查询一个内部节点,利用二分查找来决定查找哪一个分支,重复上述行为直到叶子节点,最后将叶子节点与排好序的数组进行匹配。
在节点内所有的查询都由if-else构成,在内部节点进行二分查找时,一直比较左边的键值是否不小于要查询的键值,当找到第一个比要查询的键值小时,停止比较并进入右边的分支(如果找不到这样的值,就进入最左边的分支)。这样可以保证当在节点中有复制的值时,我们可以在所有复制的键值中找到最左边的键值。
3.3 LevelCSS-Tree
对于每个节点有m个记录的Full CSS-Tree,有严格的m个键值,所有的记录都会被利用到。对于m=2t,我们定义每个节点只有只有m-1条记录,并且有一个分支因子m。一棵Level CSS-Tree树比一棵相应的Full CSS-Tree树的深度大,因为分支因子是m而不是m+1,然后对于每一个节点,需要的同伴数更少。若N为一个已排好序的数组元素所对应的节点数,Level CSS-Tree有logmN层,而Full CSS-Tree有logm+1N层。每个节点的同伴数是t,而Full CSS-tree是t*(1+2/(m+1)),所以Level CSS-tree的总的同伴数是logmN*t=log2N,而Full CSS-tree是logm+1N*t*(1+2/(m+1))=log2N*logm+1m*(1+2(m+1)).因此,Level CSS-Tree所需的companion数比Full CSS-tree少。另一方面,Level CSS-Tree需要logmN个cache accesses,遍历logmN个节点,而Full CSS-Tree需logm+1N。
构建一棵Level CSS-Tree与Full CSS-Tree类似,我们也可以利用每个节点的空槽,来存储最后一个分支的最大值,来避免遍历整棵子树来获取最大元素值。查询一棵Level CSS-Tree也与查询Full CSS-Tree类似,唯一的不同就是子节点偏移量的计算。
3.4 CSB+-Tree
3.4.1Introduction
尽管CSS-Tree相比二分查找和T-Trees查询性能更好,但是它是用于决策支持的有着相对静态数据的工作负载设计的。CSB+-Tree(CacheSensitive B+-Trees)[4],是B+-Trees的变体,连续存储给定节点的子节点,并且只存储节点的第一个子节点的地址,其他子节点的地址可以通过相对这个子节点的偏移量计算获得。由于只存储一个子节点的指针,cache的利用率是很高的,与B+-Tree类似,CSB+-Tree支持增量更新。
CSB+-Tree有两种变体,分段CSB+-Tree(SegmentedCSB+-tree)和完全CSB+-tree(FullCSB+-Tree).分段CSB+-Tree将子节点分段,在同一段的子节点连续存储,在每个节点中,只有每一段的起始地址才会被存储。当有分裂时,分段CSB+-Tree可以减少复制开销,因为只有一个分段需要移动。完全CSB+-Tree为整个节点重新分配空间,因此减少了分裂开销。
3.4.2CSB+-Tree上的操作
1) Bulkload.
对于CSB+-Tree树,一个有效的bulkload方法就是一层一层的建立索引结构。为每一个叶节点分配空间,计算在高层需要的节点数,并给该层分配连续的存储空间。通过将低层每一个节点的最大值填入高层的节点,并设置每一个高层节点的第一个子节点指针。重复上述操作直到高层只有一个节点,且这个节点为根节点。因为同一层的所有节点是连续的,所以构造节点组无需额外的复制操作。
2) Search
查询CSB+-Tree与查询B+-Tree类似,当最右边节点的键值K比要查询的键值小,给第一个子节点增加K的偏移量来获得子节点的地址。例如,K是节点的第三个键值,可以用一个c语句找到子节点:ch