從B樹、B+樹、B*樹談MySQL索引

前言:

動態查找樹主要有:二叉查找樹(Binary Search Tree),平衡二叉查找樹(Balanced Binary Search Tree),紅黑樹(Red-Black Tree ),B-tree/B±tree/ B*-tree (B~Tree)。前三者是典型的二叉查找樹結構,其查找的時間複雜度O(log2N)與樹的深度相關,那麼降低樹的深度自然會提高查找效率。

在開始介紹B-tree之前,先了解下相關的硬件知識,才能很好的瞭解爲什麼需要B-tree這種外存數據結構。(費了九牛二虎之力,終於在角落翻出滿是灰塵的《計算機組成原理》

存儲器

計算機存儲設備一般分爲兩種:主存儲器(main memory)和外存儲器(external memory)。
內存存取速度快,但容量小,價格昂貴,而且不能長期保存數據(在不通電情況下數據會消失)。
外存儲器—磁盤是一種直接存取的存儲設備(DASD)。它是以存取時間變化不大爲特徵的。可以直接存取任何字符組,且容量大、速度較其它外存設備更快。

主存儲器(main memory)—內存
目前計算機使用的主存基本都是隨機讀寫存儲器(RAM),現代RAM的結構和存取原理比較複雜,這裏本文拋卻具體差別,抽象出一個十分簡單的存取模型來說明RAM的工作原理。
在這裏插入圖片描述
從抽象角度看,主存是一系列的存儲單元組成的矩陣,每個存儲單元存儲固定大小的數據。每個存儲單元有唯一的地址,現代主存的編址規則比較複雜,這裏將其簡化成一個二維地址:通過一個行地址和一個列地址可以唯一定位到一個存儲單元。上圖展示了一個4 x 4的主存模型。

主存的存取過程如下:

當系統需要讀取主存時,則將地址信號放到地址總線上傳給主存,主存讀到地址信號後,解析信號並定位到指定存儲單元,然後將此存儲單元數據放到數據總線上,供其它部件讀取。

寫主存的過程類似,系統將要寫入單元地址和數據分別放在地址總線和數據總線上,主存讀取兩個總線的內容,做相應的寫操作。

這裏可以看出,主存存取的時間僅與存取次數呈線性關係,因爲不存在機械操作,兩次存取的數據的“距離”不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是一樣的。

外存儲器(external memory)—磁盤

與主存不同,磁盤I/O存在機械運動耗費,因此磁盤I/O的時間消耗是巨大的。

磁盤是一個扁平的圓盤(與留聲機的唱片類似)。盤面上有許多稱爲磁道的圓圈,數據就記錄在這些磁道上。磁盤可以是單片的,也可以是由大小相同且同軸的圓形盤片組成,磁盤可以轉動(各個磁盤必須同步轉動)。

每一盤片上有兩個面。如下圖中所示的6片盤組爲例,除去最頂端和最底端的外側面不存儲數據之外,一共有10個面可以用來保存信息。
在這裏插入圖片描述

一般磁盤分爲固定頭盤(磁頭固定)和活動頭盤。固定頭盤的每一個磁道上都有獨立的磁頭,它是固定不動的,專門負責這一磁道上數據的讀/寫。

活動頭盤 (如上圖)的磁頭是可移動的。每一個盤面上只有一個磁頭(磁頭是雙向的,因此正反盤面都能讀寫)。它可以從該面的一個磁道移動到另一個磁道。所有磁頭都裝在同一個動臂上,因此不同盤面上的所有磁頭都是同時移動的(行動整齊劃一)。當盤片繞主軸旋轉的時候,磁頭與旋轉的盤片形成一個圓柱體。各個盤面上半徑相同的磁道組成了一個圓柱面,我們稱爲柱面 。因此,柱面的個數也就是盤面上的磁道數。

注:
磁道:盤片被劃分成一系列同心環,圓心是盤片中心,每個同心環叫做一個磁道
柱面:各個盤面上半徑相同的磁道組成了一個圓柱面,我們稱爲柱面(因此,柱面的個數也就是任意盤片上的磁道數)
扇區:磁道被沿半徑線劃分成一個個小的段,每個段叫做一個扇區,每個扇區是磁盤的最小存儲單元
在這裏插入圖片描述
上圖爲磁盤結構示意圖

數據讀/寫原理
磁盤上數據必須用一個三維地址唯一標示:柱面號、盤面號、塊號(磁道上的盤塊,也就是扇區)。
讀/寫磁盤上某一指定數據需要下面3個步驟:

  1. 首先移動臂根據柱面號使磁頭移動到所需要的柱面上,這一過程被稱爲定位或查找 。
  2. 根據盤面號來確定指定盤面上的磁道
  3. 盤面確定以後,盤片開始旋轉,將指定塊號的磁道段移動至磁頭下

訪問某一具體信息花費的時間由3部分組成:

  • 尋道時間:完成上述步驟1所需要的時間。這部分時間代價最高,最大可達到0.1s左右
  • 旋轉時間:完成上述步驟3所需要的時間。由於盤片繞主軸旋轉速度很快,一般爲7200轉/分(電腦硬盤的性能指標之一, 家用的普通硬盤的轉速一般有5400rpm(筆記本)、7200rpm幾種)。因此一般旋轉一圈大約0.0083s
  • 傳輸時間: 數據通過系統總線傳送到內存的時間,一般傳輸一個字節(byte)大概0.02us=2*10^(-8)s

局部性原理與磁盤預讀
由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分分之一,因此爲了提高效率,要儘量減少磁盤I/O。爲了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向後讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中著名的局部性原理:

當一個數據被用到時,其附近的數據也通常會馬上被使用。

所以,程序運行期間所需要的數據通常應當比較集中。

由於磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高I/O效率。

預讀的長度一般爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割爲連續的大小相等的塊,每個存儲塊稱爲一頁(在許多操作系統中,頁得大小通常爲4k),主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,然後異常返回,程序繼續運行。

小結
根據這一小節我們可以知道,磁盤I/O是影響數據讀寫性能的主要因素之一,所以在實際的生產應用中,我們應該儘可能的減少磁盤I/O

B樹、B+樹、B*樹

那麼問題來了:就是大規模數據存儲中,實現索引查詢這樣一個實際背景下,如何減少磁盤I/O呢?
有人可能會第一時間想到遠古時期*(學生時代)的經典(二叉樹)*。不過,樹節點存儲的元素數量是有限的(如果元素數量非常多的話,查找就退化成節點內部的線性查找了),這樣導致二叉查找樹結構由於樹的深度過大而造成磁盤I/O讀寫過於頻繁,進而導致查詢效率低下。這個時候B樹、B+樹…應運而生。

下面我們一起來回顧下常見的樹結構(這時的我不禁彈了彈《數據結構》上的灰塵

二叉排序樹(Binary Sort Tree)

樹上的節點是已經排好序的,具體的排序規則如下:

  • 若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值
  • 若右子樹不空,則右字數上所有節點的值均大於它的根節點的值
  • 它的左、右子樹也分別爲二叉排序數(遞歸定義)

在這裏插入圖片描述從圖中可以看出,二叉排序樹組織數據時,用於查找是比較方便的,因爲每次經過一次節點時,最多可以減少一半的可能,不過極端情況會出現所有節點都位於同一側,直觀上看就是一條直線,那麼這種查詢的效率就比較低了,因此需要對二叉樹左右子樹的高度進行平衡化處理,於是就有了平衡二叉樹(Balenced Binary Tree)

所謂“平衡”,說的是這棵樹的各個分支的高度是均勻的,它的左子樹和右子樹的高度之差絕對值小於1,這樣就不會出現一條支路特別長的情況。於是,在這樣的平衡樹中進行查找時,總共比較節點的次數不超過樹的高度,這就確保了查詢的效率(時間複雜度爲O(logn))

二、B樹(Balance tree)

B樹,也就是B-tree(注:中間的短橫線是英文連接符,不發音。所以應該讀作B樹,而不是B減樹)事實上是一種平衡的多叉查找樹。一顆階爲M的B-tree滿足以下幾個結構特性:

  • 樹的根或者是一片樹葉,或者其子兒子數在2和M之間
  • 除根外,所有非樹葉節點的兒子數在M/2(向上取整)和M之間
  • 所有的樹葉都在相同的深度上
  • 非葉節點中的信息包括[n,A0,K1,A1,K2,A2,…,Kn,An],,其中n表示該節點中保存的關鍵字個數,K爲關鍵字且Ki<Ki+1,A爲指向子樹根節點的指針。

注:

  • 階:指一顆B-tree中節點的最大兒子數(也就是節點的最大分叉數)
  • 根:沒有父親的節點
  • 樹葉:沒有兒子的節點
  • 深度:任意節點的深度爲從根到該節點的唯一路徑的長(因此,根的深度爲0)
  • 高度:任意節點的高度爲從該節點到一片樹葉的最長路徑的長(因此,樹葉的高爲0,一棵樹的高等於根的高)

下圖爲三階B樹示例

黃色的小塊存的是兒子的地址
藍色的小塊存的是關鍵字

在這裏插入圖片描述

B樹的查詢過程和二叉排序樹比較類似,從根節點依次比較每個結點,因爲每個節點中的關鍵字和左右子樹都是有序的,所以只要比較節點中的關鍵字,或者沿着指針就能很快地找到指定的關鍵字,如果查找失敗,則會返回葉子節點,即空指針。

例如在上圖中查找29:

  1. 先將根節點讀入內存,發現17<29<35(總是從根節點開始)
  2. 根據根節點的P2指針找到兒子節點,將兒子節點讀入內存,比較可得26<29<30
  3. 根據兒子節點指針找到下一個兒子,將兒子節點讀入內存,找到29,返回該葉子節點的指針(如果沒找到,則返回NULL)

B樹的特點:

1.關鍵字集合分佈在整顆樹中。
2.任何一個關鍵字出現且只出現在一個節點中。
3.搜索有可能在非葉子節點結束。
4.其搜索性能等價於在關鍵字集合內做一次二分查找。
5.由於限制了除根結點以外的非葉子結點,至少含有M/2個兒子,確保了結點的至少利用率,其最底搜索性能爲: 在這裏插入圖片描述
其中

  • M爲階,N爲關鍵字總數;所以B-樹的性能總是等價於二分查找(與M值無關),也就沒有B樹平衡的問題;
  • 由於非樹葉節點兒子數[M/2,M]的限制,在插入結點時,如果該結點已滿,需要將結點分裂爲兩個各佔M/2的結點,將該節點的中間關鍵字存入父節點
  • 若父節點已滿,重複上一個步驟
  • 刪除結點時,需將兩個不足M/2的兄弟結點合併

三、B+樹(B-樹的變體,也是一種多路搜索樹)

B+樹定義基本與B-樹同,除了:

  • B+樹的非葉子節點不保存關鍵字記錄的指針,節點中僅含有其子樹(根節點)中的最大(或最小)關鍵字,這樣使得B+樹每個非葉子節點所能保存的關鍵字大大增加
  • B+樹葉子節點保存了父節點的所有關鍵字記錄的指針,所有數據地址必須要到葉子節點才能獲取到。所以每次數據查詢的次數都一樣
  • B+樹葉子節點的關鍵字從小到大有序排列,左邊結尾數據都會保存右邊節點開始數據的指針。(這使得B+樹比B-樹更加適合範圍查找 rang )
  • 非葉子節點的子節點數=關鍵字數,根據各種資料 這裏有兩種算法的實現方式,另一種爲非葉節點的關鍵字數=子節點數-1,雖然他們數據排列結構不一樣,但其原理還是一樣的,Mysql
    的B+樹是用第一種方式實現);

在這裏插入圖片描述
B+樹的查找過程,與B樹類似,只不過查找時,如果在非葉子節點上的關鍵字等於給定值,並不終止,而是繼續沿着指針直到葉子節點位置。因此在B+樹,不管查找成功與否,每次查找都是走了一條從根到葉子節點的路徑。

B+樹的特性如下(相比於B樹):

  • 平均查詢數據更快:非葉子節點存儲的關鍵字數更多,樹的層級更少(B樹飛葉子節點也存有關鍵字信息,有些B樹可以在非葉子節點直接擊中關鍵字返回,所以並不是每一次查詢B+樹都優於B樹)
  • 查詢速度更穩定:B+樹所有關鍵字數據地址都存在葉子節點上,所以每次查找的次數都相同
  • 全樹遍歷更快:B+樹遍歷整棵樹只需要遍歷所有的葉子節點即可(範圍查詢速度遠優於B樹)
  • B樹相對於B+樹的優點是,如果經常訪問的數據離根節點很近,而B樹的非葉子節點本身存有關鍵字其數據的地址,所以這種數據檢索的時候會要比B+樹快。

四、B*

B樹是B+樹的變體,在B+樹的非根和非葉子結點再增加指向兄弟的指針,如下:
在這裏插入圖片描述
B
樹具有以下特點(相比於B+樹):

  1. B*樹定義了非葉子結點關鍵字個數至少爲(2/3)*M,即塊的最低使用率爲2/3(代替B+樹的1/2)

  2. 分類方式不同:
    B+樹的分裂:當一個結點滿時,分配一個新的結點,並將原結點中1/2的數據複製到新結點,最後在父結點中增加新結點的指針;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,所以它不需要指向兄弟的指針;

    B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼將一部分數據移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因爲兄弟結點的關鍵字範圍改變了);如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,並各複製1/3的數據到新結點,最後在父結點增加新結點的指針;

所以,B*樹分配新結點的概率比B+樹要低,空間使用率更高;

小結:

  • 二叉搜索樹:二叉樹,每個節點只存儲一個關鍵字,等於則命中,小於走左結點,大於走右結點
  • B(B-)樹:多路搜索樹
  • -樹的根或者是一片樹葉,或者其子兒子數在2和M之間
  • 除根外,所有非樹葉節點的兒子數在M/2(向上取整)和M之間
  • 所有的樹葉都在相同的深度上
  • 非葉節點中的信息包括[n,A0,K1,A1,K2,A2,…,Kn,An],,其中n表示該節點中保存的關鍵字個數,K爲關鍵字且Ki<Ki+1,A爲指向子樹根節點的指針。(任意節點的關鍵字數=其子兒子數-1)
  • B+樹:在B-樹基礎上,爲葉子結點增加鏈表指針,所有關鍵字都在葉子結點中出現,非葉子結點作爲葉子結點的索引;B+樹總是到葉子結點才命中
  • B*樹:在B+樹基礎上,爲非葉子結點也增加鏈表指針,將結點的最低利用率從1/2提高到2/3

MySQL索引

在MySQL中,索引屬於存儲引擎級別的概念,不同存儲引擎對索引的實現方式是不同的,本文主要討論MyISAM和InnoDB兩個存儲引擎的索引實現方式。

MyISAM索引實現
MyISAM引擎使用B+Tree作爲索引結構,葉節點的data域存放的是數據記錄的地址。下圖是MyISAM索引的原理圖:

在這裏插入圖片描述

這裏設表一共有三列,假設我們以Col1爲主鍵,則上圖是一個MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重複。如果我們在Col2上建立一個輔助索引,則此索引的結構如下圖所示:

在這裏插入圖片描述

同樣也是一棵B+樹,data域保存數據記錄的地址。因此,MyISAM中索引檢索的算法爲首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其data域的值,然後以data域的值爲地址,讀取相應數據記錄。

MyISAM的索引方式也叫做“非聚集”的,之所以這麼稱呼是爲了與InnoDB的聚集索引區分。

InnoDB索引實現

雖然InnoDB也使用B+Tree作爲索引結構,但具體實現方式卻與MyISAM截然不同。

第一個重大區別是InnoDB的數據文件本身就是索引文件。從上文知道,MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。
在這裏插入圖片描述

上圖是InnoDB主索引(同時也是數據文件)的示意圖,可以看到葉節點包含了完整的數據記錄。這種索引叫做聚集索引。因爲InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作爲主鍵,如果不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段作爲主鍵,這個字段長度爲6個字節,類型爲長整型。

第二個與MyISAM索引的不同是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作爲data域。例如,上圖爲定義在Col3上的一個輔助索引:

在這裏插入圖片描述

這裏以英文字符的ASCII碼作爲比較準則。聚集索引這種實現方式使得按主鍵的搜索十分高效,但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然後用主鍵到主索引中檢索獲得記錄。

瞭解不同存儲引擎的索引實現方式對於正確使用和優化索引都非常有幫助,例如知道了InnoDB的索引實現後,就很容易明白爲什麼不建議使用過長的字段作爲主鍵,因爲所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的字段作爲主鍵在InnoDB中不是個好主意,因爲InnoDB數據文件本身是一棵B+Tree,非單調的主鍵會造成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增字段作爲主鍵則是一個很好的選擇。

發佈了70 篇原創文章 · 獲贊 265 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章