爲何MySQL數據庫索引選擇使用B+樹?

數據庫常常存放了大量的數據,數據庫的查詢也是常常發生的。html

當咱們要查詢一個龐大的數據表中的一行小小的數據的時候,就像茫茫人海中找到一個對的人同樣困難... node

咱們爲了節約時間成本,咱們必定要想辦法以最快的速度找到咱們想要的數據。數據庫

學過數據結構的童鞋必定第一個想到:搜索樹,其平均複雜度是log(n),具備不錯的查詢性能。數據結構

好的,故事從一個簡單的二叉樹開始......數據庫設計

一、二叉查找樹

(1)二叉樹簡介:函數

二叉查找樹也稱爲有序二叉查找樹,知足二叉查找樹的通常性質,是指一棵空樹具備以下性質:性能

一、任意節點左子樹不爲空,則左子樹的值均小於根節點的值;this

二、任意節點右子樹不爲空,則右子樹的值均大於於根節點的值;spa

三、任意節點的左右子樹也分別是二叉查找樹;操作系統

四、沒有鍵值相等的節點;

圖片描述

上圖爲一個普通的二叉查找樹,按照中序遍歷的方式能夠從小到大的順序排序輸出:二、三、五、六、七、8。二叉樹的查找平均時間複雜度是O(log(n))。

圖片描述

你們看上圖,若是咱們的根節點選擇是最小或者最大的數,那麼二叉查找樹就徹底退化成了線性結構。

雖然咱們已經擁有了平均O(log(n))的時間複雜度,可是,人類是貪心的。咱們接受不了在極端狀況下時間複雜度退化成O(n)。

圖片描述

思來想去,極端狀況下這棵樹已經失去了平衡,若是咱們讓他恢復平衡,豈不是很美妙。那就給他取個名字叫:平衡二叉樹。

二、紅黑樹

紅黑樹就是平衡二叉樹的一種。

紅黑樹在每一個節點增長一個存儲位表示節點的顏色,能夠是red或black。經過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍。

紅黑樹的性質:

一、每一個節點非紅即黑;

二、根節點是黑的;

三、每一個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的;

四、若是一個節點是紅的,那麼它的兩兒子都是黑的;

五、對於任意節點而言,其到葉子點樹NULL指針的每條路徑都包含相同數目的黑節點;

六、每條路徑都包含相同的黑節點;

圖片描述

注意:
性質 3 中指定紅黑樹的每一個葉子節點都是空節點,並且並葉子節點都是黑色。但 Java 實現的紅黑樹將使用 null 來表明空節點,所以遍歷紅黑樹時將看不到黑色的葉子節點,反而看到每一個葉子節點都是紅色的。

性質 4 的意思是:從每一個根到節點的路徑上不會有兩個連續的紅色節點,但黑色節點是能夠連續的。
所以若給定黑色節點的個數 N,最短路徑的狀況是連續的 N 個黑色,樹的高度爲 N - 1;最長路徑的狀況爲節點紅黑相間,樹的高度爲 2(N - 1) 。

性質 5 是成爲紅黑樹最主要的條件,後序的插入、刪除操做都是爲了遵照這個規定。

(1)紅黑樹的左旋右旋

紅黑樹的左右旋是比較重要的操做,左右旋的目的是調整紅黑節點結構,轉移黑色節點位置,使其在進行插入、刪除後仍能保持紅黑樹的 5 條性質。

左旋

圖片描述

// 左旋代碼
  __leftRotate(x) {
    // x是要將要旋轉到左子樹的節點
    if (x != null) {
        // y是將要旋轉到父節點位置的節點
        var y = x.right;
        // 把y的左節點移到x的右節點 把x的右兒子改成y的左兒子
        x.right = y.left;
        if (y.left != null) {
          // 由於要移動了y的左節點 它成爲了x的兒子 因此要把y.left 的爸爸改成x
          y.left.parent = x;
        }
        
        // x 的爸爸變成了 y 的爸爸
        y.parent = x.parent;
        if (x.parent == null) {
          // 若是 x 是根節點了 那麼記得把root 換成y
          this.root = y;
        }
        else if (x.parent.left == x) {
          // 若是 x 自身本來是 左兒子 則把x 的父親的左兒子 換成 y
          x.parent.left = y;
        } 
        else {
          // 若是 x 自身本來是 右兒子 則把p 的父親的右兒子 換成 y
          x.parent.right = y;
        }
        
        // 把 x 變成 y 的左兒子
        y.left = x;
        // 把 x 的 父親變成 y
        x.parent = y;
    }
  }

右旋

圖片描述

// 右旋代碼
  __rightRotate(p) {
      if (p != null) {
          var l = p.left;
          p.left = l.right;
          if (l.right != null) l.right.parent = p;
          l.parent = p.parent;
          if (p.parent == null)
              root = l;
          else if (p.parent.right == p)
              p.parent.right = l;
          else p.parent.left = l;
          l.right = p;
          p.parent = l;
      }
  }

(2)紅黑樹的平衡插入

紅黑樹的插入主要分兩步:

一、首先和二叉查找樹的插入同樣,查找、插入

二、而後調整結構,保證知足紅黑樹狀態

對結點進行從新着色
以及對樹進行相關的旋轉操做

紅黑樹的插入在二叉查找樹插入的基礎上,爲了從新恢復平衡,繼續作了插入修復操做。

第一步:
二叉查找樹的就是一個二分查找,找到合適的位置就放進去。

__insert(node) {
    var y = null;
    var x = this.root;

    // 將紅黑樹看成一顆二叉查找樹,將節點添加到二叉查找樹中。
    while (x != null) {
        y = x;
        if(node.key < x.key) {
          x = x.left;
        }
        else {
          x = x.right;
        } 
    }
    // 找到node 將要插入的位置 y
    node.parent = y;
    if (y!=null) {
        if (node.key < y.key) {
           y.left = node;
        }        
        else {
          y.right = node;
        }
    } else {
        // 空樹
        this.root = node;
    }

    // 設置節點的顏色爲紅色
    node.color = 'RED';

    // 將它從新修正 使其符合紅黑樹
    this.insertFixUp(node);
  }

第二步:插入後調整紅黑樹結構

紅黑樹的第 5 條特徵規定,任一節點到它子樹的每一個葉子節點的路徑中都包含一樣數量的黑節點。也就是說當咱們往紅黑樹中插入一個黑色節點時,會違背這條特徵。

同時第 4 條特徵規定紅色節點的左右孩子必定都是黑色節點,當咱們給一個紅色節點下插入一個紅色節點時,會違背這條特徵。

前面說了,插入一個節點後要擔憂違反特徵 4 和 5,數學裏最經常使用的一個解題技巧就是把多個未知數化解成一個未知數。咱們這裏採用一樣的技巧,把插入的節點直接染成紅色,這樣就不會影響特徵 5,只要專心調整知足特徵 4 就行了。這樣比同時知足 四、5 要簡單一些。

染成紅色後,咱們只要關心父節點是否爲紅,若是是黑的,直接插入,無需調整。若是是紅的,就要把父節點進行變化,讓父節點變成黑色,或者換一個黑色節點當父親,這些操做的同時不能影響 不一樣路徑上的黑色節點數一致的規則。

注:插入後咱們主要關注插入節點的父親節點的位置,而父親節點位於左子樹或者右子樹的操做是相對稱的,這裏咱們只介紹一種,即插入位置的父親節點爲左子樹。

插入、染紅後的調整有 2 種狀況:

狀況1.父親節點和叔叔節點都是紅色:

圖片描述

假設插入的是節點 N,這時父親節點 P 和叔叔節點 U 都是紅色,爺爺節點 G 必定是黑色。

紅色節點的孩子不能是紅色,這時無論 N 是 P 的左孩子仍是右孩子,只要同時把 P 和 U 染成黑色,G 染成紅色便可。這樣這個子樹左右兩邊黑色個數一致,也知足特徵 4。

可是這樣改變後 G 染成紅色,G 的父親若是是紅色豈不是又違反特徵 4 了?
這個問題和咱們插入、染紅後一致,所以須要以 爺爺節點 G 爲新的調整節點,再次進行調整操做,以此循環,直到父親節點不是紅的,就沒有問題了。

狀況2.父親節點爲紅色,叔叔節點爲黑色:

圖片描述

假設插入的是節點 N,這時父親節點 P 是紅色,叔叔節點 U 是黑色,爺爺節點 G 必定是黑色。

紅色節點的孩子不能是紅色,可是直接把父親節點 P 塗成黑色也不行,這條路徑多了個黑色節點。怎麼辦呢?

既然改變不了你,那咱們就此別過吧,我換一個更適合個人!

咱們怎麼把 P 弄走呢?看來看去,仍是右旋最合適,經過把 爺爺節點 G 右旋,P 變成了這個子樹的根節點,G 變成了 P 的右子樹。

右旋後 G 跑到了右子樹上,這時把 P 變成黑的,多了一個黑節點,再把 G 變成紅的,就平衡了!

上面講的是插入節點 N 在父親節點 P 的左孩子位置,若是 N 是 P 的右孩子,就須要多進行一次左旋,把狀況化解成上述狀況。

圖片描述

N 位於 P 的右孩子位置,將 P 左旋,就化解成上述狀況了。

下面是 RBTree 在插入後進行調整的代碼:

/*
 * 紅黑樹插入修正函數
 *
 * 在向紅黑樹中插入節點以後(失去平衡),再調用該函數;
 * 目的是將它從新塑形成一顆紅黑樹。
 *
 * 參數說明:
 *     node 插入的結點  
 */
  insertFixUp(node) {
    var parent, gparent;

    // 若「父節點存在,而且父節點的顏色是紅色」
    while (((parent = node.parent)!=null) && this.__isRed(parent)) {
        gparent = parent.parent;

        //若「父節點」是「祖父節點的左孩子」
        if (parent == gparent.left) {
            // Case 1條件:叔叔節點是紅色
            var uncle = gparent.right;
            if ((uncle!=null) && this.__isRed(uncle)) {
                this.__setBlack(uncle);
                this.__setBlack(parent);
                this.__setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2條件:叔叔是黑色,且當前節點是右孩子
            if (parent.right == node) {
                var tmp;
                this.__leftRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3條件:叔叔是黑色,且當前節點是左孩子。
            this.__setBlack(parent);
            this.__setRed(gparent);
            this.__rightRotate(gparent);
        } else {    //若「z的父節點」是「z的祖父節點的右孩子」
            // Case 1條件:叔叔節點是紅色
            var uncle = gparent.left;
            if ((uncle!=null) && this.__isRed(uncle)) {
                this.__setBlack(uncle);
                this.__setBlack(parent);
                this.__setRed(gparent);
                node = gparent;
                continue;
            }

            // Case 2條件:叔叔是黑色,且當前節點是左孩子
            if (parent.left == node) {
                var tmp;
                this.__rightRotate(parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3條件:叔叔是黑色,且當前節點是右孩子。
            this.__setBlack(parent);
            this.__setRed(gparent);
            this.__leftRotate(gparent);
        }
    }

    // 將根節點設爲黑色
    this.__setBlack(this.root);
  }

OK,到目前爲止,咱們已經構建好一個紅黑樹了。咱們能夠打印出來瞧瞧:

var rb_tree = new RBTree();
rb_tree.insert(1);
rb_tree.insert(2);
rb_tree.insert(3);
rb_tree.insert(4);
rb_tree.insert(5);
rb_tree.insert(6);
rb_tree.insert(7);
rb_tree.insert(8);
rb_tree.insert(9);
rb_tree.insert(10);
console.log(rb_tree.getRoot());

圖片描述

在線生成紅黑樹地址 https://sandbox.runjs.cn/show...

紅黑樹的應用:

一、普遍用於C++的STL中,Map和Set都是用紅黑樹實現的;

二、著名的Linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊,進程的虛擬內存區域都存儲在一顆紅黑樹上,每一個虛擬地址區域都對應紅黑樹的一個節點,左指針指向相鄰的地址虛擬存儲區域,右指針指向相鄰的高地址虛擬地址空間;

三、IO多路複用epoll的實現採用紅黑樹組織管理sockfd,以支持快速的增刪改查;

四、Nginx中用紅黑樹管理timer,由於紅黑樹是有序的,能夠很快的獲得距離當前最小的定時器;

五、Java中TreeMap的實現;

誒呀,費了一大把勁終於把紅黑樹講完了。不過仍是有收穫滴~ 咱們剛剛的操做提升了查找的性能。

對於數據庫的查找問題,感受看到了但願!不過,等等。。。

牽扯到一個問題了,不考慮每種數據結構的前提條件而選擇數據結構都是在耍流氓。

紅黑樹基本都是存儲在內存中才會使用的數據結構,那磁盤中會有什麼不一樣呢?

這就要牽扯到磁盤的存儲原理了

操做系統讀寫磁盤的基本單位是扇區,而文件系統的基本單位是簇(Cluster)。

也就是說,磁盤讀寫有一個最少內容的限制,即便咱們只須要這個簇上的一個字節的內容,咱們也要含着淚把一整個簇上的內容讀完。

那麼,如今問題就來了

一個父節點只有 2 個子節點,並不能填滿一個簇上的全部內容啊?那多餘的內容豈不是要浪費了?咱們怎麼才能把浪費的這部份內容利用起來呢?哈哈,答案就是 B/B+ 樹。

因爲 B/B+ 樹分支比二叉樹更多,因此相同數量的內容,B+ 樹的深度更淺,深度表明什麼?表明磁盤 io 次數啊!數據庫設計的時候 B+ 樹有多少個分支都是按照磁盤一個簇上最多能放多少節點設計的啊!

因此,涉及到磁盤上查詢的數據結構,通常都用 B/B+ 樹啦。

三、B/B+樹

B/B+樹是爲了磁盤或其它存儲設備而設計的一種平衡多路查找樹(相對於二叉,B樹每一個內節點有多個分支),與紅黑樹相比,在相同的的節點的狀況下,一顆B/B+樹的高度遠遠小於紅黑樹的高度(在下面B/B+樹的性能分析中會提到)。B/B+樹上操做的時間一般由存取磁盤的時間和CPU計算時間這兩部分構成,而CPU的速度很是快,因此B樹的操做效率取決於訪問磁盤的次數,關鍵字總數相同的狀況下B樹的高度越小,磁盤I/O所花的時間越少。

(1)B樹

B樹也稱B-樹,它是一顆多路平衡查找樹。咱們描述一顆B樹時須要指定它的階數,階數表示了一個結點最多有多少個孩子結點,通常用字母m表示階數。當m取2時,就是咱們常見的二叉搜索樹。

一顆m階的B樹定義以下:

1)每一個結點最多有m-1個關鍵字。

2)根結點最少能夠只有1個關鍵字。

3)非根結點至少有Math.ceil(m/2)-1個關鍵字。

4)每一個結點中的關鍵字都按照從小到大的順序排列,每一個關鍵字的左子樹中的全部關鍵字都小於它,而右子樹中的全部關鍵字都大於它。

5)全部葉子結點都位於同一層,或者說根結點到每一個葉子結點的長度都相同。

圖片描述

上圖是一顆階數爲4的B樹。在實際應用中的B樹的階數m都很是大(一般大於100),因此即便存儲大量的數據,B樹的高度仍然比較小。每一個結點中存儲了關鍵字(key)和關鍵字對應的數據(data),以及孩子結點的指針。咱們將一個key和其對應的data稱爲一個記錄。但爲了方便描述,除非特別說明,後續文中就用key來代替(key, value)鍵值對這個總體。在數據庫中咱們將B樹(和B+樹)做爲索引結構,能夠加快查詢速速,此時B樹中的key就表示鍵,而data表示了這個鍵對應的條目在硬盤上的邏輯地址。

B樹的插入操做:

插入操做是指插入一條記錄,即(key, value)的鍵值對。若是B樹中已存在須要插入的鍵值對,則用須要插入的value替換舊的value。若B樹不存在這個key,則必定是在葉子結點中進行插入操做。

1)根據要插入的key的值,找到葉子結點並插入。

2)判斷當前結點key的個數是否小於等於m-1,若知足則結束,不然進行第3步。

3)以結點中間的key爲中心分裂成左右兩部分,而後將這個中間的key插入到父結點中,這個key的左子樹指向分裂後的左半部分,這個key的右子支指向分裂後的右半部分,而後將當前結點指向父結點,繼續進行第3步。

下面以5階B樹爲例,介紹B樹的插入操做,在5階B樹中,結點最多有4個key,最少有2個key

圖片描述
圖片描述
圖片描述

找到了一個很是適合數據庫索引的數據結構,感受本身棒棒噠~

圖片描述

可是,人類的貪心永無止境...
咱們想一想,B樹的每一個節點都有data域(指針),這無疑增大了節點大小,說白了增長了磁盤IO次數(磁盤IO一次讀出的數據量大小是固定的,單個數據變大,每次讀出的就少,IO次數增多,一次IO多耗時啊!),是否是咱們能夠除了葉子節點其它節點並不存儲數據,節點小,磁盤IO次數就少。
若是全部的Data域在葉子節點,那咱們將全部的葉子節點用指針串起來。這樣遍歷葉子節點就能得到所有數據,這樣就能進行區間訪問啦。遍歷效率忽然一下提升了!並且在數據庫中基於範圍的查詢是很是頻繁的,而B樹不支持這樣的操做(或者說效率過低)

(2)B+樹

圖片描述

各類資料上B+樹的定義各有不一樣,一種定義方式是關鍵字個數和孩子結點個數相同。這裏咱們採起維基百科上所定義的方式,即關鍵字個數比孩子結點個數小1,這種方式是和B樹基本等價的。上圖就是一顆階數爲4的B+樹。

除此以外B+樹還有如下的要求。

1)B+樹包含2種類型的結點:內部結點(也稱索引結點)和葉子結點。根結點自己便可以是內部結點,也能夠是葉子結點。根結點的關鍵字個數最少能夠只有1個。

2)B+樹與B樹最大的不一樣是內部結點不保存數據,只用於索引,全部數據(或者說記錄)都保存在葉子結點中。

3) m階B+樹表示了內部結點最多有m-1個關鍵字(或者說內部結點最多有m個子樹),階數m同時限制了葉子結點最多存儲m-1個記錄。

4)內部結點中的key都按照從小到大的順序排列,對於內部結點中的一個key,左樹中的全部key都小於它,右子樹中的key都大於等於它。葉子結點中的記錄也按照key的大小排列。

5)每一個葉子結點都存有相鄰葉子結點的指針,葉子結點自己依關鍵字的大小自小而大順序連接。

B+樹的插入:

圖片描述
圖片描述
圖片描述
圖片描述

B/B+樹性能分析

B-Tree中一次檢索最多須要h-1次I/O(根節點常駐內存),漸進複雜度爲O(h)=O(logmN)。通常實際應用中,m是很是大的數字,一般超過100,所以h很是小(一般不超過3)。
綜上所述,用B-Tree做爲索引結構效率是很是高的。
而紅黑樹這種結構,h明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性,因此紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差不少。

圖片描述

原諒我太懶了,畫圖很麻煩,我copy了不少網上優秀的解析裏的圖片,下面貼出連接:
https://www.cnblogs.com/nullz...

相關文章
相關標籤/搜索