最近无事,研究了一下数据库索引。大部分索引都是采用B+tree,而B+tree又是btree的优化。就先来了解一下Btree。


作为一个索引,一般是采用Key-Value的方式来存储内容。Key表示索引的关键字,而Value表示索引内容存放的位置,假设是硬盘中的某个位置,或者说是一个数据文件的偏移量。于是这样就可以根据索引的内容来查询文件的位置。


说到这里,就会产生一个疑问。既然是key-value类型,可以有很多数据结构表示,例如hash-table,搜索复杂度在O(1),为什么要使用Btree呢,而且Btree的复杂度是O(t*log(n,t)),显然不如hash-table。


这个原因有以下几种:

1.Btree是一种外排序的数据结构,Btree的使用主要是因为一旦索引的数据量过大,无法把索引载入内存,这时候需要有一种方法来减少硬盘的读取。其实要是能把索引载完全载入内存,就算10亿个数扫一遍也用不了多久。单纯一个索引文件会有那么大么?我们来算一下,一个有10亿条数据的索引该有多大。

假设key是一个字符串,一个ascii字符占一个字节。再假设我们的字符串只支持英文大小写,那么表示10亿个数所有组合要5个字节,按32位字对齐,我们假设用8个字节来表示key。对一个字符串来说这已经相当少了,对于这么大量的数据来说基本不可能。而value值是数据条目在硬盘中的偏移,我们假设硬盘采用48位的LBA编址,一个value就是8字节。那么一条key-value的索引就是16个字节。16字节*10亿=1.5G。如果key的长度再长点,那么一个索引大概在2~3G之间。如果是64g内存的话也许不算什么,单是如果内存稍小点,还要缓存数据,那么就不太够用了。而且hash-table有一个特征,就是如果一旦负载过高,hash的冲突率升高,性能退化会极其严重。一般的hash-table,需要事先设计好能承载的容量,而btree则是可以平滑扩容的。

2.最明显的不同。由于btree是有序索引,所以btree支持范围查找。而hash是无序的,不支持偏序查找,只能查固定的某个值。这个杀伤力太大了


好了,说了这么些,来正式说一下btree吧。

Btree的原理类似二叉搜索树,搜索比当前节点大的,就搜索右子树,搜索比当前节点小的,就搜索左子树。btree的每个节点保存的数据都是一个有序数列,有序数列的分叉是子节点的指针。同时最左右两侧

都是到子节点的指针。这样,子节点的指针数量永远比父节点的数据数量大1个。

比如这样  :

                                                             2        4

                                                           /       |       \

                                                         1       3        5


假如我想要找3这个数据,先在上层节点搜索,发现2和4,那么3这个数据如果有的话就一定在中间这个子节点。

由这个特征可以得出一个结论。就是右边的数据一定会大于等于左侧的数据。也就是2节点右侧的子节点的数据一定>=2,而2节点左侧的数据一定<=2。同时,<=2且最接近2的数据会在2的左子节点的最右下位置,>=2且最接近2的数据在2的右子树的最左下位置


至于搜索效率,二叉树理想情况下搜索效率和二分搜索相当。但是极端情况二叉树会退化成一条直线,不是左边没节点,就是右边没节点,搜索的平均效率退化成O(n)。

而btree的最大特征是,有所子节点,一定会在同一个高度。无论如何都不会出现一边子树长,一边子树短的情况。

为了达到这个目的,btree采用了一个非常巧妙的方式。与传统的树非常不同,btree增加树高度的方式不是向子节点中添加新的子节点。而是从树叉处分裂。一个树叉分裂成2个高度一样的小树,一旦根节点分裂,那么树的高度就增加1。

例如高度为零的   1  2  3,一旦分裂就变成了            

         2          

      /       \

   1          3

由于分裂的子树高度一样。,所以树的所有子节点都在同一层上。

怎么保证这个特性呢?

首先引用一个图论中的定义:树中子节点最少的节点的子节点数量称为这棵树的度数。(好绕口)

我们用t表示这个度数

btree中对所有非根节点的所含的数据数量有这样一个要求,最少不能少于t-1,最大不能超过2t-1。根节点可以少于t-1但不能超过2t-1。t >=2

插入的时候一旦要超过这个数量,就把树分裂。删除的时候一旦要少于这个数量,要么从兄弟节点接一个节点来充数,如果兄弟节点也不够,就把子树合并。

这样,保证了btree节点最多数据的时候数据的数是一个奇数。因为2*t-1==2*(t-1)+1,所以一个满节点可以分裂成一个至少有一个数据的根,外加2个最小的节点。

我们通过对一颗t=2的树顺序插入[0,1,2,3,4,5,6,7,8,9]这10个数来模拟这个过程。

根据定义,这颗树最少数据的节点数据数1,最大的为3


前三个数很简单   0 1 2,插入3的时候发现树的节点已经满了,于是将树分裂再插入,变成

    1              继续插入4,   变成    1           插入5的时候发现234满了,于是分裂234,把3提到上一层

  /      \                                               /     \            

 0         23                                     0      234

变成

                  1     3                                                    1        3                                      1        3      5      

               /       |       \                                            /         |        \                               /       |         |        \

           0        2        45      再插入6,变成    0          2       456     插入7  ,0        2       4         67


再插入8的的时候,发现135已经满了,二话不说直接先分裂,再插入8(注意这个过程,由于插入是由上到下检查是否该分裂,所以不会存在下层节点满,而他的父节点同时也满的情况,可以保证无论如何,分裂要提出的中位数都能添加到父节点中,并让父节点的节点数不超过2t-1)

     树变成了                       插入  9 ,寻找到678的时候分裂并插入。最后变成          

                            3                                                                                                               3    

                        /         \                                                                                                       /          \

                    1              5                                                                                                1            5  7                                                                                                

                 /      \          /    \                                                                                            /    \       /     |    \

              0        2       4     678                                                                                  0      2    4     6     89


如论是插入和搜索都很好理解。可是删除呢?比如上面这棵树,除了删除,剩下无论直接删哪个节点都会让它不能满足btree定义的条件。另外也无法直接合并,怎么办?

btree在删除节点的时复杂一点,但是逻辑是,从上到下搜索,先看自己是不是叶子节点,如果是,数据直接删除(一开始就发现自己是叶子节点除非只有一个树根)如果不是,再看数据是不是在自己这个节点上。如果在自己身上,就检查这个节点的左右子节点哪个子节点的数据大于t-1,假设左边有多余的,按照这个递归算法,去删除左边子树的最大的节点,来顶替这个要删除的节点。如果右边有多余的,就采用右边最小的。如果左右两个子树的树根都等于t-1,那么合并之,再删除。

如果要的数据不在自己身上,就判断出会在哪个子节点上。如果这个子节点等于t-1,就看他的兄弟节点能不能借数据给他。如果不能借,就把这个子树合并。

总之,无论如何,保证这个要删除的子树的沿路永远有可以借出的数据。用来确保树不变形。


看起来很复杂,还是拿上面这棵树举例子吧。假设我想删除上面这颗树中2这个数。

先找到3,发现可能在左边,但是拿到左边节点一看,不行,左边已经只有1这一个数了,已经是t-1了,

于是找右边,发现是俩数57。决定从这个节点借出他最小的数据。问题就变成了从  

                                  5     7        

                                /     |      \

                              4     6      89

中删除最小的数。按照这个算法,知道最小的数在5的左侧子节点。但是拿出来一看,不行,只有4一个数,再看5右边的子节点一看,只有6也不行。干脆合并。这棵子树变成了

           7

        /      \

     456    89


终于满足条件了,找到456一看,还是叶子节点,直接把4删除。这个4用来借给左边的节点。

怎么借?看下图。

            3                                                                4

        /       \                                                           /     \

     1           7           +4   --------->                     1        7

   /    \        /   \                                                  /    \     /    \

0        2    56   89                                          0     23  56 89

算法就是用借来的数顶替自己,把自己插到子节点右下角的位置。

借完以后,发现左边一个1,右边一个7,不行,还得合并,变成了

                        1      4       7              

                     /       |       |        \          

                  0       23    56       89

再看,发现2可能在1和4中间的子节点。拿到一看,满足条件,再一看,还是叶子节点。2直接删除,最后这颗树变成了这样:

                  1        4       7

              /         |         |        \

          0           3        56     89      

嗯,还是一棵完好的Btree。

写了点代码来表现这个过程:

#!/usr/bin/env python
from random import randint,choice
from bisect import bisect_left
from collections import deque
class InitError(Exception):
    pass
class ParaError(Exception):
    pass
class KeyValue(object):
    __slots__=('key','value')
    def __init__(self,key,value):
        self.key=key
        self.value=value
    def __str__(self):
        return str((self.key,self.value))
    def __cmp__(self,key):
        if self.key>key:
            return 1
        elif self.key==key:
            return 0
        else:
            return -1
class BtreeNode(object):
    def __init__(self,t,parent=None):
        if not isinstance(t,int):
            raise InitError,'degree of Btree must be int type'
        if t<2:
            raise InitError,'degree of Btree must be equal or greater then 2'
        else:
            self.vlist=[]
            self.clist=[]
            self.parent=parent
            self.__degree=t
    @property
    def degree(self):
        return self.__degree
    def isleaf(self):
        return len(self.clist)==0
    def traversal(self):
        result=[]
        def get_value(n):
            if n.clist==[]:
                result.extend(n.vlist)
            else:
                for i,v in enumerate(n.vlist):
                    get_value(n.clist[i])
                    result.append(v)
                get_value(n.clist[-1])
        get_value(self)
        return result
    def show(self):
        q=deque()
        h=0
        q.append([self,h])
        while True:
            try:
                w,hei=q.popleft()
            except IndexError:
                return
            else:
                print [v.key for v in w.vlist],'the height is',hei
                if w.clist==[]:
                    continue
                else:
                    if hei==h:
                        h+=1
                    q.extend([[v,h] for v in w.clist])
    def getmax(self):
        n=self
        while not n.isleaf():
            n=n.clist[-1]
        return (n.vlist[-1],n)
    def getmin(self):
        n=self
        while not n.isleaf():
            n=n.clist[0]
        return (n.vlist[0],n)
class IndexFile(object):
    def __init__(fname,cellsize):
        f=open(fname,'wb')
        f.close()
        self.name=fname
        self.cellsize=cellsize
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
    def write_obj(obj,pos):
           pass
    def read_obj(obj,pos):
        pass
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
class Btree(object):
    def __init__(self,t):
        self.__degree=t
        self.__root=BtreeNode(t)
    @property
    def degree(self):
        return self.__degree
    def traversal(self):
        """
        use dfs to search a btree's node
        """
        return self.__root.traversal()
    def show(self):
        """
        use bfs to show a btree's node and its height
        """
        return self.__root.show()
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
    def search(self,mi=None,ma=None):
        """
        search key-value pair within range mi<=key<=ma.
        if mi or ma is not specified,the searching range
        is key>=mi or key <=ma
        """
        result=[]
        node=self.__root
        if mi is None and ma is None:
            raise ParaError,'you need setup searching range'
        elif mi is not None and ma is not None and mi>ma:
            raise ParaError,'upper bound must be greater or equal than lower bound'
        def search_node(n):
            if mi is None:
                if not n.isleaf():
                    for i,v in enumerate(n.vlist):
                        if v<=ma:
                            result.extend(n.clist[i].traversal())
                            result.append(v)
                        else:
                            search_node(n.clist[i])
                            return
                    search_node(n.clist[-1])
                else:
                    for v in n.vlist:
                        if v<=ma:
                            result.append(v)
                        else:
                            break
            elif ma is None:
                if not n.isleaf():
                    for i,v in enumerate(n.vlist):
                        if v<mi:
                            continue
                        else:
                            search_node(n.clist[i])
                            while i<len(n.vlist):
                                result.append(n.vlist[i])
                                result.extend(n.clist[i+1].traversal())
                                i+=1
                            break
                    if v.key<mi:
                        search_node(n.clist[-1])
                else:
                    for v in n.vlist:
                        if v>=mi:
                            result.append(v)
            else:
                if not n.isleaf():
                    for i,v in enumerate(n.vlist):
                        if v<mi:
                            continue
                        elif mi<=v<=ma:
                            search_node(n.clist[i])
                            result.append(v)
                        elif v>ma:
                            search_node(n.clist[i])
                    if v<=ma:
                        search_node(n.clist[-1])
                else:
                    for v in n.vlist:
                        if mi<=v<=ma:
                            result.append(v)
                        elif v>ma:
                            break
        search_node(node)
        return result
    def insert(self,key_value):
        node=self.__root
        full=self.degree*2-1
        mid=full/2+1
        def split(n):
            new_node=BtreeNode(self.degree,parent=n.parent)
            new_node.vlist=n.vlist[mid:]
            new_node.clist=n.clist[mid:]
            for c in new_node.clist:
                c.parent=new_node
            if n.parent is None:
                newroot=BtreeNode(self.degree)
                newroot.vlist=[n.vlist[mid-1]]
                newroot.clist=[n,new_node]
                n.parent=new_node.parent=newroot
                self.__root=newroot
            else:
                i=n.parent.clist.index(n)
                n.parent.vlist.insert(i,n.vlist[mid-1])
                n.parent.clist.insert(i+1,new_node)
            n.vlist=n.vlist[:mid-1]
            n.clist=n.clist[:mid]
            return n.parent
        def insert_node(n):
            if len(n.vlist)==full:
                insert_node(split(n))
            else:
                if n.vlist==[]:
                    n.vlist.append(key_value)
                else:
                    if n.isleaf():
                        p=bisect_left(n.vlist,key_value)  #locate insert point in ordered list vlist
                        n.vlist.insert(p,key_value)
                    else:
                        p=bisect_left(n.vlist,key_value)
                        insert_node(n.clist[p])
        insert_node(node)
    def delete(self,key_value):
        node=self.__root
        mini=self.degree-1
        def merge(n,i):
            n.clist[i].vlist=n.clist[i].vlist+[n.vlist[i]]+n.clist[i+1].vlist
            n.clist[i].clist=n.clist[i].clist+n.clist[i+1].clist
            n.clist.remove(n.clist[i+1])
            n.vlist.remove(n.vlist[i])
            if n.vlist==[]:
                n.clist[0].parent=None
                self.__root=n.clist[0]
                del n
                return self.__root
            else:
                return n
        def tran_l2r(n,i):
            left_max,left_node=n.clist[i].getmax()
            right_min,right_node=n.clist[i+1].getmin()
            right_node.vlist.insert(0,n.vlist[i])
            del_node(n.clist[i],left_max)
            n.vlist[i]=left_max
        def tran_r2l(n,i):
            left_max,left_node=n.clist[i].getmax()
            right_min,right_node=n.clist[i+1].getmin()
            left_node.vlist.append(n.vlist[i])
            del_node(n.clist[i+1],right_min)
            n.vlist[i]=right_min
        def del_node(n,kv):
            p=bisect_left(n.vlist,kv)
            if not n.isleaf():
                if p==len(n.vlist):
                    if len(n.clist[-1])>mini:
                        del_node(n.clise[p],kv)
                    elif len(n.clist[p-1])>mini:
                        tran_l2r(n,p-1)
                        del_node(n.clist[-1],kv)
                    else:
                        del_node(merge(n,p-1),kv)
                else:
                    if n.vlist[p]==kv:
                        left_max,left_node=n.clist[i].getmax()
                        if len(n.clist[p].vlist)>mini:
                            del_node(n.clist[p],left_max)
                            n.vlist[p]=left_max
                        else:
                            right_min,right_node=n.clist[i+1].getmin()
                            if len(n.clist[p+1].vlist)>mini:
                                del_node(n.clist[p+1],right_min)
                                n.vlist[p]=right_min
                            else:
                                del_node(merge(n,p),kv)
                    else:
                        if len(n.clist[p].vlist)>mini:
                            del_node(n.clist[p],kv)
                        elif len(n.clist[p+1].vlist)>mini:
                            tran_r2l(n,p)
                            del_node(n.clist[p],kv)
                        else:
                            del_node(merge(n,p),kv)
            else:
                try:
                    pp=n.vlist[p]
                except IndexError:
                    return -1
                else:
                    if pp!=kv:
                        return -1
                    else:
                        n.vlist.remove(kv)
                        return 0
        del_node(node,key_value)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           
def test():
    mini=50
    maxi=200
    testlist=[]
    for i in range(1,1001):
        key=randint(1,1000)
        #key=i
        value=choice('abcdefg')
        testlist.append(KeyValue(key,value))
    mybtree=Btree(5)
    for x in testlist:
        mybtree.insert(x)
    print 'my btree is:\n'
    mybtree.show()
    #mybtree.delete(testlist[0])
    #print '\n the newtree is:\n'
    #mybtree.show()
    print '\nnow we are searching item between %d and %d\n\n'%(mini,maxi)
    print [v.key for v in mybtree.search(mini,maxi)]
    #for x in mybtree.traversal():
    #    print x
if __name__=='__main__':
    test()


注意的是以上代码只是在内存上模拟的btree的插入删除算法。而真实的btree的每个节点都是要放到硬盘上的。也就是每个btree 中children_list里存放的是该节点在硬盘上的地址,或者是文件偏移。本来也想做个来着,可是python这种动态数据类型的想要把数据格式化存放到硬盘最好的方式是把btree-node 变成ctype,真的还不如用c做。如果用c,那么为了表现key-value中key的多种类型,还要借助c++模板,太过麻烦了。好多年没碰过c++了,再说上学那会也根本没好好学,遂作罢。

另外,最后再看一下,btree的性能。

btree的性能分成两部分,一是节点载入的次数,也就是读取硬盘的次数,这个肯定是最为主要的性能影响部分。二是计算这个算法所要花费的cpu时间。跟硬盘读取速度比可以说忽略不计了。内存和硬盘速度要差6个数量级,这就是一百万倍的差距,弥补这个差距太难了。

再搜索时候,最差情况是搜索到叶子节点,硬盘的读取次数取决于树的高度。

假设只有一个树根的时候,高度为0。我再假设树的度数为t。树最高的时候,每个节点的数最少,那么为t-1

树根只有1个树,第一层有2* (t-1)。第二层每个t-1,又有t个子树,就是2t个(t-1)。假设高度为h,那么h层就有2*t的h方个。

假设节点的总数为n。那么n=1+2*(t-1)**1+...2*(t-1)**h。

一个初中生一眼就看出这是一个等比数列求和问题。等式两边同时乘t-1再一减。得到n=2*t**h-1.

得到树的最大高度是以t为底(n+1)/2的对数。

我们就算取t=2,就是树最高的情况下,也能有log(n)的性能。10亿个数假设树的度数为500,那么树的高度只有3.2层!最多只需要读4次硬盘就能找到数据。

(ps,层数中没有计算根节点,是每次操作都会涉及根节点,因此根节点一定是被cache到内存中的)

我们再来思考一下,btree的度数是如何决定的。

btree的一个节点应该是最适合硬盘读取的量。硬盘计算的单位是扇区512字节,但操作系统是按照簇来算的,我们假设是标准的4k。一个4k能容纳多少度数的一个btree节点取决于btree索引的key和value的大小,假设地址8字节,key-value 24字节,其他忽略不计。也就是(2t-1)*24+2t*8=4*1024 得到t=64。10亿个数最多读5次数据。如果我再把节点的容量变大,那么3次以内可以保证读到数据。

读取看完了,再来看插入和删除。

每次插入节点至少要搜索h次磁盘并至少写入一次。也就是插入性能和没有索引比至少低一倍。如果树的沿路都要分裂,每次分裂还要写回磁盘3次,那么3层b数每次写操作最多多出3次读操作和10来次写操作。在t很大的情况下,分裂的索引读取和写回的操作是少不了的。

删除就更不用说了,性能影响会更加严重。

当然,删除是可以优化的,就是标记起来暂时不删除。

尽管如此,读操作上获得的千万倍的速度提升是要远远超过这些负面影响。


最后,关于btree还有几点。

1.key-value中有相同的key增加了搜索的算法复杂度。因为即使找到key以后,还需要继续搜索。但是这个问题对于实际情况没什么影响。因为对于一颗度数很大,层数很少的btree来讲,绝大多数的数据都存在在叶子节点上。相同key分布在中间节点的概率本就不大,还要分布在中间节点的边缘,这个概率更加小。

2.删除btree节点远比想象中要复杂。这是因为删除一个数据之前,肯定伴随一次查找,有可能满足这个查找的节点很多,但只删除一个。这种情况不能根据key来删,只能根据key-value来删。这个查找的过程,明显是可以用来优化删除方法的。我的程序中并没有考虑这个问题。

3. 这个btree索引只能支持前缀匹配搜索,按照正常的字典序来计算搜索。但是对中缀和后缀搜索无能为力。

直白点说就是只支持like 'a%',不支持like '%a%'和like '%a'。

4.由于节点插入的和存放无序性,读一段连续数据会变成完全随机读取。完全遍历的方法也不是很优雅。

5.btree-node的数据结构上是可以不含指向父节点的指针的,加上这个,能方便一点


先写这么些,累死了。不对的地方,还希望看到的人及时指正