天天看點

MySQL的InnoDB索引原理詳解

摘要:

  本篇介紹下Mysql的InnoDB索引相關知識,從各種樹到索引原理到存儲的細節。

  InnoDB是Mysql的預設存儲引擎(Mysql5.5.5之前是MyISAM,文檔)。本着高效學習的目的,本篇以介紹InnoDB為主,少量涉及MyISAM作為對比。

  這篇文章是我在學習過程中總結完成的,内容主要來自書本和部落格(參考文獻會給出),過程中加入了一些自己的了解,描述不準确的地方煩請指出。

  1 各種樹形結構

  本來不打算從二叉搜尋樹開始,因為網上已經有太多相關文章,但是考慮到清晰的圖示對了解問題有很大幫助,也為了保證文章完整性,最後還是加上了這部分。

  先看看幾種樹形結構:

  1 搜尋二叉樹:每個節點有兩個子節點,資料量的增大必然導緻高度的快速增加,顯然這個不适合作為大量資料存儲的基礎結構。

  2 B樹:一棵m階B樹是一棵平衡的m路搜尋樹。最重要的性質是每個非根節點所包含的關鍵字個數 j 滿足:┌m/2┐ - 1 <= j <= m - 1;一個節點的子節點數量會比關鍵字個數多1,這樣關鍵字就變成了子節點的分割标志。一般會在圖示中把關鍵字畫到子節點中間,非常形象,也容易和後面的B+樹區分。由于資料同時存在于葉子節點和非葉子結點中,無法簡單完成按順序周遊B樹中的關鍵字,必須用中序周遊的方法。

  3 B+樹:一棵m階B樹是一棵平衡的m路搜尋樹。最重要的性質是每個非根節點所包含的關鍵字個數 j 滿足:┌m/2┐ - 1 <= j <= m;子樹的個數最多可以與關鍵字一樣多。非葉節點存儲的是子樹裡最小的關鍵字。同時資料節點隻存在于葉子節點中,且葉子節點間增加了橫向的指針,這樣順序周遊所有資料将變得非常容易。

  4 B*樹:一棵m階B樹是一棵平衡的m路搜尋樹。最重要的兩個性質是1每個非根節點所包含的關鍵字個數 j 滿足:┌m2/3┐ - 1 <= j <= m;2非葉節點間添加了橫向指針。

MySQL的InnoDB索引原理詳解
MySQL的InnoDB索引原理詳解
MySQL的InnoDB索引原理詳解

  B/B+/B*三種樹有相似的操作,比如檢索/插入/删除節點。這裡隻重點關注插入節點的情況,且隻分析他們在目前節點已滿情況下的插入操作,因為這個動作稍微複雜且能充分展現幾種樹的差異。與之對比的是檢索節點比較容易實作,而删除節點隻要完成與插入相反的過程即可(在實際應用中删除并不是插入的完全逆操作,往往隻删除資料而保留下空間為後續使用)。

  先看B樹的分裂,下圖的紅色值即為每次新插入的節點。每當一個節點滿後,就需要發生分裂(分裂是一個遞歸過程,參考下面7的插入導緻了兩層分裂),由于B樹的非葉子節點同樣儲存了鍵值,是以已滿節點分裂後的值将分布在三個地方:1原節點,2原節點的父節點,3原節點的建立兄弟節點(參考5,7的插入過程)。分裂有可能導緻樹的高度增加(參考3,7的插入過程),也可能不影響樹的高度(參考5,6的插入過程)。

MySQL的InnoDB索引原理詳解

  B+樹的分裂:當一個結點滿時,配置設定一個新的結點,并将原結點中1/2的資料複制到新結點,最後在父結點中增加新結點的指針;B+樹的分裂隻影響原結點和父結點,而不會影響兄弟結點,是以它不需要指向兄弟節點的指針。

MySQL的InnoDB索引原理詳解

  B*樹的分裂:當一個結點滿時,如果它的下一個兄弟結點未滿,那麼将一部分資料移到兄弟結點中,再在原結點插入關鍵字,最後修改父結點中兄弟結點的關鍵字(因為兄弟結點的關鍵字範圍改變了)。如果兄弟也滿了,則在原結點與兄弟結點之間增加新結點,并各複制1/3的資料到新結點,最後在父結點增加新結點的指針。可以看到B*樹的分裂非常巧妙,因為B*樹要保證分裂後的節點還要2/3滿,如果采用B+樹的方法,隻是簡單的将已滿的節點一分為二,會導緻每個節點隻有1/2滿,這不滿足B*樹的要求了。是以B*樹采取的政策是在本節點滿後,繼續插入兄弟節點(這也是為什麼B*樹需要在非葉子節點加一個兄弟間的連結清單),直到把兄弟節點也塞滿,然後拉上兄弟節點一起湊份子,自己和兄弟節點各出資1/3成立新節點,這樣的結果是3個節點剛好是2/3滿,達到B*樹的要求,皆大歡喜。

MySQL的InnoDB索引原理詳解

  B+樹适合作為資料庫的基礎結構,完全是因為計算機的記憶體-機械硬碟兩層存儲結構。記憶體可以完成快速的随機通路(随機通路即給出任意一個位址,要求傳回這個位址存儲的資料)但是容量較小。而硬碟的随機通路要經過機械動作(1磁頭移動 2盤片轉動),通路效率比記憶體低幾個數量級,但是硬碟容量較大。典型的資料庫容量大大超過可用記憶體大小,這就決定了在B+樹中檢索一條資料很可能要借助幾次磁盤IO操作來完成。如下圖所示:通常向下讀取一個節點的動作可能會是一次磁盤IO操作,不過非葉節點通常會在初始階段載入記憶體以加快通路速度。同時為提高在節點間橫向周遊速度,真實資料庫中可能會将圖中藍色的CPU計算/記憶體讀取優化成二叉搜尋樹(InnoDB中的page directory機制)。

MySQL的InnoDB索引原理詳解

  真實資料庫中的B+樹應該是非常扁平的,可以通過向表中順序插入足夠資料的方式來驗證InnoDB中的B+樹到底有多扁平。我們通過如下圖的CREATE語句建立一個隻有簡單字段的測試表,然後不斷添加資料來填充這個表。通過下圖的統計資料(來源見參考文獻1)可以分析出幾個直覺的結論,這幾個結論宏觀的展現了資料庫裡B+樹的尺度。

  1 每個葉子節點存儲了468行資料,每個非葉子節點存儲了大約1200個鍵值,這是一棵平衡的1200路搜尋樹!

  2 對于一個22.1G容量的表,也隻需要高度為3的B+樹就能存儲了,這個容量大概能滿足很多應用的需要了。如果把高度增大到4,則B+樹的存儲容量立刻增大到25.9T之巨!

  3 對于一個22.1G容量的表,B+樹的高度是3,如果要把非葉節點全部加載到記憶體也隻需要少于18.8M的記憶體(如何得出的這個結論?因為對于高度為2的樹,1203個葉子節點也隻需要18.8M空間,而22.1G從良表的高度是3,非葉節點1204個。同時我們假設葉子節點的尺寸是大于非葉節點的,因為葉子節點存儲了行資料而非葉節點隻有鍵和少量資料。),隻使用如此少的記憶體就可以保證隻需要一次磁盤IO操作就檢索出所需的資料,效率是非常之高的。

MySQL的InnoDB索引原理詳解

  2 Mysql的存儲引擎和索引

  可以說資料庫必須有索引,沒有索引則檢索過程變成了順序查找,O(n)的時間複雜度幾乎是不能忍受的。我們非常容易想象出一個隻有單關鍵字組成的表如何使用B+樹進行索引,隻要将關鍵字存儲到樹的節點即可。當資料庫一條記錄裡包含多個字段時,一棵B+樹就隻能存儲主鍵,如果檢索的是非主鍵字段,則主鍵索引失去作用,又變成順序查找了。這時應該在第二個要檢索的列上建立第二套索引。  這個索引由獨立的B+樹來組織。有兩種常見的方法可以解決多個B+樹通路同一套表資料的問題,一種叫做聚簇索引(clustered index ),一種叫做非聚簇索引(secondary index)。這兩個名字雖然都叫做索引,但這并不是一種單獨的索引類型,而是一種資料存儲方式。對于聚簇索引存儲來說,行資料和主鍵B+樹存儲在一起,輔助鍵B+樹隻存儲輔助鍵和主鍵,主鍵和非主鍵B+樹幾乎是兩種類型的樹。對于非聚簇索引存儲來說,主鍵B+樹在葉子節點存儲指向真正資料行的指針,而非主鍵。

  InnoDB使用的是聚簇索引,将主鍵組織到一棵B+樹中,而行資料就儲存在葉子節點上,若使用"where id = 14"這樣的條件查找主鍵,則按照B+樹的檢索算法即可查找到對應的葉節點,之後獲得行資料。若對Name列進行條件搜尋,則需要兩個步驟:第一步在輔助索引B+樹中檢索Name,到達其葉子節點擷取對應的主鍵。第二步使用主鍵在主索引B+樹種再執行一次B+樹檢索操作,最終到達葉子節點即可擷取整行資料。

  MyISM使用的是非聚簇索引,非聚簇索引的兩棵B+樹看上去沒什麼不同,節點的結構完全一緻隻是存儲的内容不同而已,主鍵索引B+樹的節點存儲了主鍵,輔助鍵索引B+樹存儲了輔助鍵。表資料存儲在獨立的地方,這兩顆B+樹的葉子節點都使用一個位址指向真正的表資料,對于表資料來說,這兩個鍵沒有任何差别。由于索引樹是獨立的,通過輔助鍵檢索無需通路主鍵的索引樹。

  為了更形象說明這兩種索引的差別,我們假想一個表如下圖存儲了4行資料。其中Id作為主索引,Name作為輔助索引。圖示清晰的顯示了聚簇索引和非聚簇索引的差異。

MySQL的InnoDB索引原理詳解
MySQL的InnoDB索引原理詳解

  我們重點關注聚簇索引,看上去聚簇索引的效率明顯要低于非聚簇索引,因為每次使用輔助索引檢索都要經過兩次B+樹查找,這不是多此一舉嗎?聚簇索引的優勢在哪?

  1 由于行資料和葉子節點存儲在一起,這樣主鍵和行資料是一起被載入記憶體的,找到葉子節點就可以立刻将行資料傳回了,如果按照主鍵Id來組織資料,獲得資料更快。

  2 輔助索引使用主鍵作為"指針" 而不是使用位址值作為指針的好處是,減少了當出現行移動或者資料頁分裂時輔助索引的維護工作,使用主鍵值當作指針會讓輔助索引占用更多的空間,換來的好處是InnoDB在移動行時無須更新輔助索引中的這個"指針"。也就是說行的位置(實作中通過16K的Page來定位,後面會涉及)會随着資料庫裡資料的修改而發生變化(前面的B+樹節點分裂以及Page的分裂),使用聚簇索引就可以保證不管這個主鍵B+樹的節點如何變化,輔助索引樹都不受影響。

  3 Page結構

  如果說前面的内容偏向于解釋原理,那後面就開始涉及具體實作了。

  了解InnoDB的實作不得不提Page結構,Page是整個InnoDB存儲的最基本構件,也是InnoDB磁盤管理的最小機關,與資料庫相關的所有内容都存儲在這種Page結構裡。Page分為幾種類型,常見的頁類型有資料頁(B-tree Node)Undo頁(Undo Log Page)系統頁(System Page) 事務資料頁(Transaction System Page)等。單個Page的大小是16K(編譯宏UNIV_PAGE_SIZE控制),每個Page使用一個32位的int值來唯一辨別,這也正好對應InnoDB最大64TB的存儲容量(16Kib * 2^32 = 64Tib)。一個Page的基本結構如下圖所示:

MySQL的InnoDB索引原理詳解

  每個Page都有通用的頭和尾,但是中部的内容根據Page的類型不同而發生變化。Page的頭部裡有我們關心的一些資料,下圖把Page的頭部詳細資訊顯示出來:

MySQL的InnoDB索引原理詳解

  我們重點關注和資料組織結構相關的字段:Page的頭部儲存了兩個指針,分别指向前一個Page和後一個Page,頭部還有Page的類型資訊和用來唯一辨別Page的編号。根據這兩個指針我們很容易想象出Page連結起來就是一個雙向連結清單的結構。

MySQL的InnoDB索引原理詳解

  再看看Page的主體内容,我們主要關注行資料和索引的存儲,他們都位于Page的User Records部分,User Records占據Page的大部分空間,User Records由一條一條的Record組成,每條記錄代表索引樹上的一個節點(非葉子節點和葉子節點)。在一個Page内部,單連結清單的頭尾由固定内容的兩條記錄來表示,字元串形式的"Infimum"代表開頭,"Supremum"代表結尾。這兩個用來代表開頭結尾的Record存儲在System Records的段裡,這個System Records和User Records是兩個平行的段。InnoDB存在4種不同的Record,它們分别是1主鍵索引樹非葉節點 2主鍵索引樹葉子節點 3輔助鍵索引樹非葉節點 4輔助鍵索引樹葉子節點。這4種節點的Record格式有一些差異,但是它們都存儲着Next指針指向下一個Record。後續我們會詳細介紹這4種節點,現在隻需要把Record當成一個存儲了資料同時含有Next指針的單連結清單節點即可。

MySQL的InnoDB索引原理詳解

  User Record在Page内以單連結清單的形式存在,最初資料是按照插入的先後順序排列的,但是随着新資料的插入和舊資料的删除,資料實體順序會變得混亂,但他們依然保持着邏輯上的先後順序。

MySQL的InnoDB索引原理詳解

  把User Record的組織形式和若幹Page組合起來,就看到了稍微完整的形式。

MySQL的InnoDB索引原理詳解

  現在看下如何定位一個Record:

  1 通過根節點開始周遊一個索引的B+樹,通過各層非葉子節點最終到達一個Page,這個Page裡存放的都是葉子節點。

  2 在Page内從"Infimum"節點開始周遊單連結清單(這種周遊往往會被優化),如果找到該鍵則成功傳回。如果記錄到達了"supremum",說明目前Page裡沒有合适的鍵,這時要借助Page的Next Page指針,跳轉到下一個Page繼續從"Infimum"開始逐個查找。

MySQL的InnoDB索引原理詳解

  詳細看下不同類型的Record裡到底存儲了什麼資料,根據B+樹節點的不同,User Record可以被分成四種格式,下圖種按照顔色予以區分。

  1 主索引樹非葉節點(綠色)

  1 子節點存儲的主鍵裡最小的值(Min Cluster Key on Child),這是B+樹必須的,作用是在一個Page裡定位到具體的記錄的位置。

  2 最小的值所在的Page的編号(Child Page Number),作用是定位Record。

  2 主索引樹葉子節點(黃色)

  1 主鍵(Cluster Key Fields),B+樹必須的,也是資料行的一部分

  2 除去主鍵以外的所有列(Non-Key Fields),這是資料行的除去主鍵的其他所有列的集合。

  這裡的1和2兩部分加起來就是一個完整的資料行。

  3 輔助索引樹非葉節點非(藍色)

  1 子節點裡存儲的輔助鍵值裡的最小的值(Min Secondary-Key on Child),這是B+樹必須的,作用是在一個Page裡定位到具體的記錄的位置。

  2 主鍵值(Cluster Key Fields),非葉子節點為什麼要存儲主鍵呢?因為輔助索引是可以不唯一的,但是B+樹要求鍵的值必須唯一,是以這裡把輔助鍵的值和主鍵的值合并起來作為在B+樹中的真正鍵值,保證了唯一性。但是這也導緻在輔助索引B+樹中非葉節點反而比葉子節點多了4個位元組。(即下圖中藍色節點反而比紅色多了4位元組)

  3 最小的值所在的Page的編号(Child Page Number),作用是定位Record。

  4 輔助索引樹葉子節點(紅色)

  1 輔助索引鍵值(Secondary Key Fields),這是B+樹必須的。

  2 主鍵值(Cluster Key Fields),用來在主索引樹裡再做一次B+樹檢索來找到整條記錄。

MySQL的InnoDB索引原理詳解

  下面是本篇最重要的部分了,結合B+樹的結構和前面介紹的4種Record的内容,我們終于可以畫出一幅全景圖。由于輔助索引的B+樹與主鍵索引有相似的結構,這裡隻畫出了主鍵索引樹的結構圖,隻包含了"主鍵非葉節點"和"主鍵葉子節點"兩種節點,也就是上圖的的綠色和黃色的部分。

MySQL的InnoDB索引原理詳解

  把上圖還原成下面這個更簡潔的樹形示意圖,這就是B+樹的一部分。注意Page和B+樹節點之間并沒有一一對應的關系,Page隻是作為一個Record的儲存容器,它存在的目的是便于對磁盤空間進行批量管理,上圖中的編号為47的Page在樹形結構上就被拆分成了兩個獨立節點。

MySQL的InnoDB索引原理詳解

  至此本篇就算結束了,本篇隻是對InnoDB索引相關的資料結構和實作進行了一些梳理總結,并未涉及到Mysql的實戰經驗。這主要是基于幾點原因:

  1 原理是基石,隻有充分了解InnoDB索引的工作方式,我們才有能力高效的使用好它。

  2 原理性知識特别适合使用圖示,我個人非常喜歡這種表達方式。

  3 關于InnoDB優化,在《高性能Mysql》裡有更加全面的介紹,對優化Mysql感興趣的同學完全可以自己擷取相關知識,我自己的積累還未達到能分享這些内容的地步。

  另:對InnoDB實作有更多興趣的同學可以看看Jeremy Cole的部落格(參考文獻三篇文章的來源),這位老兄曾先後在Mysql,Yahoo,Twitter,Google從事資料庫相關工作,他的文章非常棒!

參考文獻:

[1] Jeremy Cole The physical structure of InnoDB index pages

[2] Jeremy Cole B+Tree index structures in InnoDB

[3] Jeremy Cole The physical structure of records in InnoDB

[4] 姜承堯  MySQL技術内幕-InnoDB存儲引擎 第二版

[5] Schwartz,B / Zaitsev,P / Tkach 高性能Mysql 第三版

[6] B-tree wiki