MySQL B+樹索引結構

看了很多關于MySQL B+樹索引的文檔,但一直有些問題沒搞明白:

  • B+樹索引在磁盤上是怎么存儲的?
  • 內節點、葉子節點的物理結構又是什么樣子的?

直到看到一份資料《MySQL 是怎樣運行的:從根兒上理解 MySQL》,基本解決了我的現有疑惑。但好的資料就是看著看著又能讓你思考更多問題,有新的疑惑,再去解決新的疑惑讓自己不斷提升。下面我將把從這個資料學習到的B+樹索引結構的知識按自己的邏輯整理后分享給大家,以此角度來給大家安利這份學習資料,大綱如下:

  • B+樹索引結構的推導過程

    1. 數據行結構--單向鏈表;
    2. 數據頁結構--通過頁目錄使用二分法快速找到目標行;
    3. 目錄項--多個數據頁時如何定位到目標行所在的數據頁;
    4. B+樹索引--目錄項太多,大目錄嵌套小目錄,就形成了B+樹
  • 聚簇索引

    1. 特點;
    2. 主鍵的選擇以及原因。
  • 二級索引結構和特點

  • MyISAM 索引結構和特點

B+樹索引結構的推導過程

1. 數據行結構

一切要從數據行結構說起,這里特指 InnoDB 行結構:

看起來很復雜,不過此文章只需要關注數據行結構的記錄頭信息中有個 next_record 結構,它表示從當前記錄的真實數據到下一條記錄的真實數據的地址偏移量,所以在一個數據頁里行與行根據這個設計可以組成一個有序的單向鏈表(按照主鍵排序):
InnoDB行--單向鏈表
2. 數據頁結構

一個數據頁默認16KB,可以存放很多行數據,那如何在一個數據頁中快速找到一行數據呢?在數據頁中有個叫“頁目錄”的結構:

  • 把數據頁里的數據行按規則分成 n 個組;
  • 每個組中主鍵最大的那一行數據的地址偏移量存到“頁目錄”的“槽”中。

這樣就可以根據主鍵值使用二分法進行快速查找到目標行所在位置:

多個數據頁之間,根據頁結構中的 File Header 中的 FIL_PAGE_PREV、FIL_PAGE_NEXT 記錄上一個頁號和下一個頁號,把許許多多頁建立一個雙向鏈表串聯起來:
3. 目錄項

如果一張表的數據存在很多個數據頁里,如何找到目標行數據在哪一個數據頁中呢?

簡單的實現方法是給這些數據頁做一個目錄:取出每個數據頁中最小那行數據的主鍵值,和頁號(page_no),組成一行數據,也稱為目錄項,記錄到目錄中。這樣也可以通過二分法來查找一個指定的主鍵值在哪個數據頁上:

但是對目錄使用二分法查找的前提是:這個目錄的內容(目錄項)是連續存放在某個地方的。但實際上 IoonDB 的存儲的最小單位是頁,默認只有 16K,所以這個方法在實現上不可行。

由于目錄中的內容“目錄項”跟真實的用戶記錄類似:

  • 每個數據頁中最小的主鍵值;
  • 頁號,也叫 page_no。

所以可以用數據行結構、頁結構來存儲目錄項,為了和真實的用戶記錄做區分,在數據行結構的記錄頭信息中把目錄項紀錄的 record_type 屬性設置為“1”,而普通的用戶記錄則為“0”。所以目錄項放到數據頁中就被設計成這樣了:
4. B+樹索引

如上圖,如果一張表的行數很多,勢必就會有很多數據頁,那么就可能出現一個頁存不下所有“目錄項紀錄”的情況,所以可能會有很多個“目錄項數據頁”,那又怎么快速知道應該在哪個“目錄項數據頁”上查找目標數據在哪個“數據頁”上呢?

也很簡單,再為“目錄項數據頁”生成一個更高一級的目錄即可,即大目錄嵌套小目錄,直到最頂級的那個目錄只需要一個“頁”就能存下第二級目錄的所有目錄項:

這就是 B+樹索引結構:

  • 實際用戶記錄其實都存放在B+樹的最底層的節點上,這些節點也被稱為葉子節點或葉節點;
  • 其余用來存放目錄項的節點稱為非葉子節點或者內節點;
  • 其中 B+樹最上邊的那個節點也稱為根節點。
5. 時間復雜度

索引樹的高度決定了查找數據的速度,而索引樹的高度 h 取決于:

  • 每個葉子節點(即數據頁)中能存放多少條“用戶記錄”,m;
  • 每個內節點或非葉子節點能存放多少條“目錄項記錄”,n;
  • 表的總行數 X。

m*n^(h-1)=X ,則 ??1=log????/?? ,這就是常說的B+樹索引查找的時間復雜度為O(log n) 的由來。(一個頁面最少存儲2條記錄的設計,就是為了讓索引的效果更好)

假設每行數據大約1K,innodb 數據頁大小默認為16K,也就是大約每個葉子結點可以存放16條用戶記錄,即 m=16;
主鍵ID一般為bigint 類型,占 8 字節,指針大小在 InnoDB 源碼中設置為 6 字節,這樣一共 14 字節,所以每個非葉子結點大約可以存放 16384/14=1170 條目錄項紀錄,即n=1170;
要想控制樹高為3,則表的總行數應該是:16*1170^(3-1)=21902400

所以InnoDB表行數一般建議在 2000 萬行以內,這樣查詢效率較高。

聚簇索引

1. 特點

其實聚簇索引的特點屬于老生常談了,但按例還是得介紹一下。
在 InnoDB 中聚簇索引(主鍵)就是數據的存儲方式(所有的用戶記錄都存儲在了葉子節點),也就是所謂的索引即數據,數據即索引。

  • 使用記錄主鍵值的大小進行記錄和頁的排序,這包括三個方面的含義:

    1. 頁內的記錄是按照主鍵的大小順序排成一個單向鏈表;
    2. 各個存放用戶記錄的頁也是根據頁中用戶記錄的主鍵大小順序排成一個雙向鏈表;
    3. 存放目錄項記錄的頁分為不同的層次,在同一層次中的頁也是根據頁中目錄項記錄的主鍵大小順序排成一個雙向鏈表。
  • B+樹的葉子節點存儲的是完整的用戶記錄。
    所謂完整的用戶記錄,就是指這個記錄中存儲了所有列的值(包括隱藏列)。

2. 主鍵的選擇

聚簇索引是底層的說法,而主鍵是運維層面的說法(因為有語法 primary key(id)),通常主鍵的選擇是:id int auto_increment,為什么呢?

  • int 或者 bigint 類型占用字節少節省空間,因為二級索引的葉子節點、非葉子節點上都要保存主鍵鍵值;
  • 索引是有序的,auto_increment 自增屬性會保證按順序插入數據,不會造成數據頁的分裂(因為數據頁中數據行是按照主鍵的順序組成的單向鏈表,數據一旦變化,是需要維護這種順序的),減少性能開銷;
  • id 字段沒有業務含義,本身不會被更新,所以記錄基本不會挪動在數據頁中的位置。

二級索引

二級索引是一個與聚簇索引獨立的 B+ 樹,葉子節點不再保存完整的用戶記錄,只保存索引列鍵值和主鍵鍵值,二級索引中無論是數據行之間的單向鏈表還是數據頁之間的雙向鏈表都是按照二級索引列的鍵值進行排序的:

注意這里畫的內節點只包含索引列的值和頁號,但實際上還應包含主鍵值,原文中后面是有單獨一章“內節點中目錄項記錄的唯一性”,說明存主鍵值是為了解決有二級索引允許重復值,但在B+樹索引結構中需要唯一性,這樣插入數據才能按序插入到準確位置。

MyISAM

MyISAM 存儲引擎與 InnoDB 是不一樣的,數據和索引分開存放:

  • 將表中的記錄按照記錄的插入順序單獨存儲在一個文件中,稱之為數據文件。這個文件并不劃分為若干個數據頁,有多少記錄就往這個文件中塞多少記錄就成了。我們可以通過行號而快速訪問到一條記錄;
  • 使用MyISAM存儲引擎的表會把索引信息另外存儲到一個稱為索引文件的另一個文件中。
    MyISAM表的主鍵索引,葉子節點中存儲的不是完整的用戶記錄,而是主鍵值 + 行號的組合。也就是先通過索引找到對應的行號,再通過行號去找對應的記錄;
    二級索引類似,葉子節點存儲的是對應字段的值 + 行號。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容