1. 程式人生 > >淺析——B樹,B+樹,B*樹以及分析MySQL的兩種引擎

淺析——B樹,B+樹,B*樹以及分析MySQL的兩種引擎

接觸到了資料結構當中的B樹,B+樹,B*樹,我覺得應該寫一篇部落格記錄下,畢竟是第一次接觸的,只有寫了部落格以後,感覺對這個的印象才會更加深刻。
前言:
為什麼要有B樹?
學習任何一個東西我們都要知道為什麼要有它,B樹也一樣,既然儲存資料,我們為什麼不用紅黑樹呢?

這個要從幾個方面來說了,
計算機有一個區域性性原理,就是說,當一個數據被用到時,其附近的資料也通常會馬上被使用。
所以當你用紅黑樹的時候,你一次只能得到一個鍵值的資訊,而用B樹,可以得到最多M-1個鍵值的資訊。這樣來說B樹當然更好了。
另外一方面,同樣的資料,紅黑樹的階數更大,B樹更短,這樣查詢的時候當然B樹更具有優勢了,效率也就越高。

一.B樹

首先我們來談一談關於B樹的問題,

對於B樹,我們首先要知道它的應用,B樹大量應用在資料庫和檔案系統當中。

B樹是對二叉查詢樹的改進。它的設計思想是,將相關資料儘量集中在一起,以便一次讀取多個數據,減少硬碟操作次數。

B樹為系統最優化大塊資料的讀和寫操作。B樹演算法減少定位記錄時所經歷的中間過程,從而加快存取速度。普遍運用在資料庫和檔案系統。

假定一個節點可以容納100個值,那麼3層的B樹可以容納100萬個資料,如果換成二叉查詢樹,則需要20層!假定作業系統一次讀取一個節點,並且根節點保留在記憶體中,那麼B樹在100萬個資料中查詢目標值,只需要讀取兩次硬碟。

B 樹可以看作是對2-3查詢樹的一種擴充套件,即他允許每個節點有M-1個子節點。

B樹的結構要求:
1)根節點至少有兩個子節點
2)每個節點有M-1個key,並且以升序排列
3)位於M-1和M key的子節點的值位於M-1 和M key對應的Value之間
4)其它節點至少有M/2個子節點
5)所有葉子節點都在同一層
這裡寫圖片描述
根據B樹的特點,我們首先可以寫出B樹的整體的結構。

1.B樹結構

 B樹的結構我們定義需要參考規則,我們首先是需要給出儲存鍵值的一個數組,這個陣列的大小取決與我們定義的M,然後我們根據規則,可以得到一個儲存M+1個子的一個數組,然後當然為了方便訪問,parent指標,然後要有一個記錄每個節點中鍵值個數的一個size。

所以定義如下:
template <typename
K,int M> struct BTreeNode { K _keys[M]; //用來儲存鍵值。 BTreeNode<K, M>* _sub[M + 1]; //用來儲存子。 BTreeNode<K, M>* _parent; size_t _size; BTreeNode() :_parent(NULL) , _size(0) { int i = 0; for ( i = 0; i < M; i++) { _keys[i] = K(); _sub[i] = K(); } _sub[i] = K(); } };

2.B樹的查詢

對於AVL,BST,紅黑樹,B樹這些高階的資料結構而言,查詢演算法是非常重要的。我們首先確定返回值,對於這種關於key和key-value的資料結構,參考map和set,我們讓它返回一個pair的一個結構體。
pair結構體的定義在std中是

template<typename K,typename V>
struct pair
{
    K key;
    V value;
}

我們只需要讓這個裡面的value變為bool值,value返回以後說明的是存不存就可以了。

接下來的思路就是從根節點進行和這個節點當中的每一個key比較,如果=那麼就返回找到了,如果小於,那麼就到這個節點左面的子節點中找,如果大了,就繼續向後面的鍵值進行查詢。如果相等那麼就返回。

示例程式碼:

pair <Node*,int > Find(const K &key)
    {
        Node* cur = _root;
        Node* parent = NULL;
        while (cur)
        {
            size_t i = 0;
            while (i < cur->_size)
            {
                //如果小於當前,向後
                if (cur->_keys[i] < key)
                {
                    i++;
                }
                //如果大於,
                else if (cur->_keys[i]>key)
                {
                    cur = cur->_sub[i];
                    parent = cur;
                    break;
                }
                //相等,返回這個節點
                else
                {
                    return pair<Node *, int>(NULL, -1);
                }

            }
            if (key > cur->_sub[i + 1])
            {
                cur = cur->_sub[i];
            }

            //為了防止出現我返回空指標操作,如果是空指標,那麼就返回父親
            if (cur != NULL && i == cur->_size)
            {
                parent = cur;
                cur = cur->_sub[i];
            }

        }
        return pair<Node *, int>(parent, 1);

    }

3.B樹的插入

這裡寫圖片描述
示例程式碼:


    bool Insert(const K &key)
    {
        //首先來考慮空樹的情況
        if (_root == NULL)
        {
            //給這個節點中新增key,並且讓size++。
            _root = new Node;
            _root->_keys[0] = key;
            _root->_size++;
            return true;
        }
        //使用通用的key-value結構體來儲存找到的key所在的節點。

        pair<Node*,int > ret=Find(key);

        //在這裡來看這個節點是否存在,存在就直接return false。
        if (ret.second == -1)
        {
            return false;
        }

        Node* cur = ret.first;
        K newKey = key;
        Node *sub = NULL;
        //此時表示考慮插入。
        while (1)
        {
            //向cur裡面進行插入,如果沒滿插入,滿了就進行分裂。
            InsetKey(cur, newKey, sub);

            //小於M,這樣就可以直接插入
            if (cur->_size < M)
            {
                return true;
            }

            //如果==M,那麼就應該進行分裂
            //首先找到中間的節點
            size_t mid = cur->_size / 2;
            //建立一個節點,用來儲存中間節點右邊所有的節點和子節點。
            Node * tmp = new Node;

            size_t j = 0;
            //進行移動sub以及所有的子接點。
            for (size_t i = mid + 1; i < cur->_size; i++)
            {
                tmp->_keys[j] = cur->_keys[i];
                cur->_keys[i] = K();
                cur->_size--;
                tmp->_size++;
                j++;
            }

            //移動子串
            for (j = 0; j < tmp->_size + 1; j++)
            {
                tmp->_sub[j] = cur->_sub[mid + 1 + j];
                if (tmp->_sub[j])
                {
                    tmp->_sub[j]->_parent = tmp;
                }
                cur->_sub[mid + 1 + j] = NULL;
            }



            //進行其他的移動
            //分裂的條件就是要麼分裂根,要麼就是分裂子節點,要麼就是所在節點的節點數小於M。

            //考慮根分裂,分裂的時候建立節點,然後把中間節點上拉,記得要更改最後的parent
            if (cur->_parent == NULL)
            {
                _root = new Node();
                _root->_keys[0] = cur->_keys[mid];
                cur->_keys[mid] = K();
                cur->_size--;
                _root->_size++;

                _root->_sub[0] = cur;
                cur->_parent = _root;

                _root->_sub[1] = tmp;
                tmp->_parent = _root;
                return true;
            }
            //分裂如果不是根節點,那麼就把mid節點插入到上一層節點中,然後看上一層節點是否要分裂。注意修改cur和sub

            else
            {
                newKey = cur->_keys[mid];
                cur->_keys[mid] = K();
                cur->_size--;
                cur = cur->_parent;

                sub = tmp;
                sub->_parent = cur;

            }
        }

    }
    void InsetKey(Node* cur, const K &key, Node* sub)
    {
        int i = cur->_size - 1;
        while (i>=0)
        {
            //進行插入
            if (key > cur->_keys[i])
            {
                break;
            }
            //進行移動
            else
            {
                cur->_keys[i + 1] = cur->_keys[i];
                cur->_sub[i + 2] = cur->_sub[i + 1];
            }
            i--;
        }
        //進行插入
        cur->_keys[i + 1] = key;
        //插入子
        cur->_sub[i + 2] = sub;

        //如果沒滿,只需要對size++;
        if (cur->_size < M)
        {
            cur->_size++;
        }
    }

二.B+樹

接下來介紹B樹的升級版本,

B+樹
B+樹是B-樹的變體,也是一種多路搜尋樹:
1.其定義基本與B-樹同,除了:
2.非葉子結點的子樹指標與關鍵字個數相同;
3.非葉子結點的子樹指標P[i],指向關鍵字值屬於[K[i], K[i+1])的子樹(B-樹是開區間);
5.為所有葉子結點增加一個鏈指標;
6.所有關鍵字都在葉子結點出現;
這裡寫圖片描述

B+樹相比於B樹能夠更加方便的遍歷。

B+樹簡單的說就是變成了一個索引一樣的東西。 B+的搜尋與B-樹也基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可以在非葉子結點命中),B+樹的效能相當於是給葉子節點做一次二分查詢。

B+樹只有葉子節點存的是Key-value,非葉子節點只需要儲存key就好了。

B+樹的查詢演算法:當B+樹進行查詢的時候,你首先一定需要記住,就是B+樹的非葉子節點中並不儲存節點,只存一個鍵值方便後續的操作,所以非葉子節點就是索引部分,所有的葉子節點是在同一層上,包含了全部的關鍵值和對應資料所在的地址指標。這樣其實,進行 B+樹的查詢的時候,只需要在葉子節點中進行查詢就可以了。

B+樹的插入演算法與B樹的大致思想也是一樣的,只不過在這裡的上拉就是隻把鍵值上拉。

三.B*樹

接下來要說明的就是B*樹,B*樹是對B+樹進行的又一次的升級。在B+樹的非根和非葉子結點再增加指向兄弟的指標;

這裡寫圖片描述
在B+樹基礎上,為非葉子結點也增加連結串列指標,將結點的最低利用率從1/2提高到2/3;

在這比如說當你進行插入節點的時候,它首先是放到兄弟節點裡面。如果兄弟節點滿了的話,進行分裂的時候從兄弟節點和這個節點各取出1/3,放入新建的節點當中,這樣也就實現了空間利用率從1/2到1/3。

四.關於B樹和B+樹相關應用拓展

其實B樹B+樹最需要關注的是它們的應用,B樹和B+樹經常被用於資料庫中,作為MySQL資料庫索引。索引(Index)是幫助MySQL高效獲取資料的資料結構。

為了查詢更加高效,所以採用B樹作為資料庫的索引。
在MySQL中,索引屬於儲存引擎級別的概念,不同 儲存引擎對索引的實現方式是不同的,我們接下來討論兩個引擎:MyISAM和InnoDB這兩種引擎。

1.MyISAM

這裡寫圖片描述
這裡寫圖片描述
MyISAM中有兩種索引,分別是主索引和輔助索引,在這裡面的主索引使用具有唯一性的鍵值進行建立,而輔助索引中鍵值可以是相同的。MyISAM分別會存一個索引檔案和資料檔案。它的主索引是非聚集索引。當我們查詢的時候我們找到葉子節點中儲存的地址,然後通過地址我們找到所對應的資訊。

2.InnoDB

這裡寫圖片描述
這裡寫圖片描述
InnoDB索引和MyISAM最大的區別是它只有一個數據檔案,在InnoDB中,表資料檔案本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點資料域儲存了完整的資料記錄。所以我們又把它的主索引叫做聚集索引。而它的輔助索引和MyISAM也會有所不同,它的輔助索引都是將主鍵作為資料域。所以,這樣當我們查詢的時候通過輔助索引要先找到主鍵,然後通過主索引再找到對於的主鍵,得到資訊。

這就是MySQL的兩種引擎
這兩種引擎那個好呢?

從歷史上來說MyISAM歷史更加久遠,所以InnoDB效能也就更好了,在這我們需要考慮當我們修改資料庫中的表的時候,資料庫發生了變化,那麼他們的主鍵的地址也就發生了變化,這樣你的MyISAM的主索引和輔助索引就需要進行重新建立索引。而InnoDB只需要改變主索引,因為它的輔助索引是存主鍵的。所以這樣考慮InnoDB更加高效。

另外,我們也就很容易明白為什麼不建議使用過長的欄位作為主鍵,因為所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。