本文介紹Innodb的索引資料頁面儲存結構,主要介紹資料頁面的整體結構,而頁面的詳細結構將會在另一篇中介紹。
1. B+樹
閱讀本文前,首先要知道一些關於B樹的基礎知識。Innodb的一個表可能包含多個索引,每個索引都使用B+樹來儲存。而索引包括叢集索引和二級索引,叢集索引使用表的主鍵作為索引鍵,包含表的所有欄位。二級索引只包含索引鍵和叢集索引鍵(主鍵)的內容,不包括其他欄位。每一個索引都是一棵B+樹,每棵B+樹由很多頁面組成,而每個頁面大小一般為16K。從B+樹的組織圖來看,B樹的頁面可分為:
- 葉子節點:B樹層次為0的頁面,儲存記錄的所有內容
- 非葉子節點:B樹層次大於0的頁面,只儲存索引鍵和頁面指標。
一個B+樹的結構大概為
圖1 B+樹的總體結構
從可知,相同層次的頁面是用一個雙向鏈表串連起來的。一般情況下,從B+樹的最左邊葉子節點開始,一直向右掃描,就可以得到B+樹的從小到大的所有資料。因此,對於葉子節點,有如下特徵:
- 頁內資料是按索引鍵排序的。
- 頁面的任一記錄的索引索引值不小於其左兄弟頁面的任何記錄。
那麼,下文主要介紹兩方面內容:
2. 頁面儲存格式
一個頁面的儲存格式如顯示:
圖2 Innodb頁面儲存格式
從得,一個頁面的儲存由以下幾部分組成:
1. 頁頭(Page Header):記錄頁面的控制資訊,共佔150位元組,包括頁的左右兄弟頁面指標、頁面空間使用方式等,頁頭的詳細說明會在下一篇中描述。
2. 最小虛記錄、最大虛記錄:兩個固定位置儲存的虛記錄,本身並不儲存資料。最小虛記錄比任何記錄都小,而最大虛記錄比任何記錄都大。
3. 記錄堆(record heap):指的橙黃色部分。表示頁面已指派的記錄空間,也是索引資料的真正儲存地區。記錄堆分為兩種,即有效記錄和已刪除記錄。有效記錄就是索引正常使用的記錄,而已刪除記錄表示索引已經刪除,不在使用的記錄,如的深藍色部分。隨著記錄的更新和刪除越來越頻繁,記錄堆中已刪除記錄將會越多,即會出現越來越多的空洞(片段)。這些已刪除記錄串連起來,就會成為頁面的自由空間鏈表。
4. 未配置的空間:指頁面未使用的儲存空間,隨著頁面不斷使用,未配置的空間將會越來越小。當新插入一條記錄時,首先嘗試從自由空間鏈表中獲得合適的儲存位置(空間足夠),如果沒有滿足的,就會在未配置的空間中申請。
5. slot區:slot是一些頁面有效記錄的指標,每個slot佔兩個位元組,儲存了記錄相對頁面首地址的位移。如果頁面有n條有效記錄,那麼slot的數量就在n/8+2~n/4+2之間。下一節詳細介紹slot區,它是記錄頁面有序和二分尋找的關鍵。
6. 頁尾(Page Tailer):頁面最後部分,佔8個位元組,主要儲存頁面的校正資訊。
頁面中的頁頭,最大/最小虛記錄以及頁尾都是頁面中有固定的儲存位置。而我們最關心的頁面結構可能是記錄堆以及slot區。那麼,頁面記錄如何保證有序的呢?看下節的介紹。
3. 如何保證有序
上文說到,資料頁內的資料是按索引鍵有序的。那麼怎麼保證這種有序性呢?我們先分析一些可能做法。
普通做法1:
在記錄堆中,記錄是按儲存順序排序的,即rec1<rec2<rec3...<rec_m。那麼,如果需要插入一條新記錄rec索引鍵大小在rec2和rec3之間,那麼就需要rec3記錄及以後的記錄堆內容需要向右memmov一個rec大小的空間,然後再插入新記錄rec。這樣能保證資料是有序的,但效能會非常低下。
普通做法2:
換一種做法,如果頁面記錄不是按儲存空間有序,而是使用一個鏈表串連起來,從鏈頭到鏈尾依次有序。那麼插入一條記錄,只要修改前後兩記錄的指標就行,這樣就可以避免記錄的移動同時保證了有序性。但是,帶來的問題是,鏈表是無法使用二分尋找的,這樣的設計會導致查詢效率低下。
文藝做法:
為了減少頁面記錄的移動,保證頁面資料的有序性,同時滿足二分尋找的要求,可以在頁面的尾部空間中分配了Slot區。每一個slot佔兩個位元組,儲存記錄相對頁面的位移。並且slot對應的記錄是從右往左(從高地址到低地址)依次有序的,例2中S2對應的記錄必定大於S1指向的記錄。
現假設一個slot對應一條記錄,如slot1對應rec_a,slot2對應rec_b,slot3對應rec_c,以此類推,並且rec_a<rec_b<rec_c...。那麼,如果在rec_a和rec_b之間插入新記錄rec,只要將slot2到slot_n之間的空間向左平移2位元組,原slot2空間指向新記錄rec。這樣,插入記錄就不用關心rec的儲存位置,只要平移若干slot空間就可以了,並且能保證資料按slot是有序的。
如果尋找記錄,由於每一個slot的大小固定並且空間是連續的,就可以通過二分尋找來定位記錄。
個人認為這是比較文藝的做法,但我們看看innodb比較2B的真正做法。
innodb真正的2B做法:
圖3 頁面記錄邏輯組織圖
事實上,innodb頁面記錄都已經通過一個鏈表維護起來,並且從鏈頭到鏈尾依次增大(普通方法2)。這種方式之前也已經說到,是無法使用二分尋找。為瞭解決這個問題,innodb使用若干slot指向鏈的某些記錄,而不是slot跟記錄一一對應,3所示。
Slot指向的記錄依然保證著從右往左依次有序的特性,我們使用rec[S2]表示S2指向的記錄,那麼rec[S2]>rec[S1]。另外,本文稱兩個slot之間的記錄稱為一個Slot支鏈,3虛線圈住的部分,表示S2指向的Slot支鏈。Innodb維護的Slot支鏈高度為4-8,如果一個支鏈的高度超過或不足,會導致相應的支鏈拆分和合併作業。
如何查詢?
如果在頁面中查詢記錄r1(為簡單起見,假設索引鍵是唯一的)。首先通過二分尋找定位Slot號X,滿足
rec[X-1]< r1 <= rec[X]
那麼記錄r1要麼不存在,要麼就在Slot支鏈X中。接著就是遍曆這條支鏈,找到真正記錄。但支鏈的搜尋只能一一遍曆,不能使用二分尋找。
如何插入?
首先通過查詢的方式確定插入的Slot支鏈和插入位置,在自由空間鏈表或未配置的空間中獲得空間並寫記錄內容,slot支鏈高度加1,同時維護好原鏈表的關係。
插入記錄後,如果Slot支鏈高度超過8,那麼就將該支鏈拆分為兩個子鏈,同時多申請一個slot(平移此slot及其後面的空間)。
這樣設計有什麼好處?
這種設計相當於文藝做法有什麼好處呢?想了很久,只想到了一個,就是減少了slot的分配和移動。然而,這樣設計的缺點倒是不少。
- 從儲存空間中說,雖然slot減少了,但記錄需要指標連結起來(同樣2個位元組),文藝做法是沒有必要將記錄按序連結起來的,因此這樣反而增多了空間使用(另外,支鏈高度的維護也是有空間代價的,下一篇會詳述)。
- 從存取效率上,不能完全的二分尋找,需要使用二分尋找+順序遍曆,查詢效能也有所折扣。
- 從複雜度上,大大增大代碼的複雜度。
是不是某些巧妙的地方需要這樣的設計來達到更好的效能呢?也有這種可能,但暫時還沒發現。而我所知的有些資料庫更多是用那種“文藝”的設計方案。
事實上,定長頁面的整體儲存結構都是大同小異的,主要都是由圖2的這些部分組成。具體不同可能是在於頁頭的詳細內容以及記錄的儲存格式,這些內容將會下一篇介紹。
參考
《MySQL技術內幕-InnoDB儲存引擎》
http://hedengcheng.com/?p=118