首頁 > 軟體

一文搞懂MySQL索引頁結構

2022-02-28 13:05:14

1. 前言

「頁」是InnoDB管理儲存空間的基本單位,也是記憶體和磁碟互動的基本單位。也就是說,哪怕你需要1位元組的資料,InnoDB也會讀取整個頁的資料,下次讀取的資料如果恰巧也在這個頁裡,就能命中快取了。寫也是一樣的,寫資料前要先把頁載入到記憶體,然後在記憶體中修改,該頁被記為「髒頁」,髒頁淘汰之前必須刷盤。

InnoDB有很多型別的頁,它們的用處也各不相同。比如:有存放undo紀錄檔的頁、有存放INODE資訊的頁、有存放Change Buffer資訊的頁、存放使用者記錄資料的頁等等。今天我們要聊的,就是最基礎也是最重要的,存放使用者記錄資料的「索引頁」。

2. 索引頁結構

InnoDB預設的頁大小是16KB,在初始化表空間之前可以在組態檔中進行設定,一旦初始化完成就不可再變更了。檢視頁大小的命令如下,顯示的是位元組數。

SHOW VARIABLES LIKE 'innodb_page_size';

索引頁結構如下圖所示:

索引頁由七部分組成,其中Infimum和Supremum也屬於記錄,只不過是虛擬記錄,這裡為了與使用者記錄區分開,還是決定將兩者拆開。

名稱大小描述
File Header38位元組所有頁的通用檔案頭資訊
Page Header56位元組索引頁特有的頁頭資訊
Infimum+Supremum26位元組頁中虛擬的最小、最大記錄
User Records變長使用者記錄資料
Free Space變長空閒空間
Page Directory變長頁目錄,加速頁內資料檢索效率
File Trailer8位元組所有頁的通用檔案尾資訊,校驗頁是否完整

2.1 File Header

File Header是所有頁都有的一個通用的結構,佔用固定的38位元組,它記錄了頁的一些通用的狀態資訊,例如:頁的頁號、Checksum、把頁串聯成雙向連結串列的指標、頁的型別等等。

名稱大小描述
FIL_PAGE_SPACE_OR_CHECKSUM4位元組新版本中代表頁的校驗和Checksum
FIL_PAGE_OFFSET4位元組頁號
FIL_PAGE_PREV4位元組上一個頁的頁號
FIL_PAGE_NEXT4位元組下一個頁的頁號
FIL_PAGE_LSN8位元組頁面最後被修改時的LSN值
FIL_PAGE_TYPE2位元組頁的型別
FIL_PAGE_FILE_FLUSH_LSN8位元組僅在系統表空間的第1個頁中使用,代表檔案至少被重新整理到了對應的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID4位元組頁資料哪個表空間

FIL_PAGE_SPACE_OR_CHECKSUM

基於當前頁計算出的校驗和(Checksum),可以把它看作是雜湊值,校驗和不同,則兩個頁資料肯定不同。它的作用是InnoDB在髒頁刷盤時,有可能會遇到頁刷到一半斷電的情況,頁的頭和尾部分分別記錄校驗和,只有當頭尾的校驗和一致的時候,才代表磁碟上的頁是完整的,否則就是一個損壞的頁。

FIL_PAGE_OFFSET

頁號,頁的唯一標識,全域性遞增的數位,InnoDB通過頁號來定位唯一的一個頁。4位元組儲存,意味著一個表空間最多可以有232個頁,按照一個頁16KB計算,則一個表空間最多支援64TB的資料。

FIL_PAGE_PREV & FIL_PAGE_NEXT

一個頁大小才16KB,一張表資料其實是由N多個頁構成的,頁與頁之間在物理上可以是不連續的,但是邏輯上要連續,FIL_PAGE_PREV和FIL_PAGE_NEXT分別指向當前頁的上一個頁和下一個頁的頁號,通過這兩個指標將索引頁串聯成了一個雙向連結串列。記錄與記錄之間是單向的,頁與頁之間是雙向的!

FIL_PAGE_LSN

頁面最後被修改時,對應的LSN值。LSN的全稱是Log Sequence Number,紀錄檔序列號。它是一個遞增的數位,和事務相關,這裡不作贅述。

FIL_PAGE_TYPE

當前頁的型別,InnoDB為了不同的目的設計了很多不同型別的頁,索引頁的固定值是0x45BF

FIL_PAGE_FILE_FLUSH_LSN

僅在第1個頁中使用,用來判斷資料庫是正常關閉還是異常宕機。

FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID

僅記錄當前頁資料哪個表空間。

2.2 Page Header

Page Header是索引頁特有的結構,佔用固定的56位元組,它記錄了索引頁中記錄相關的狀態資訊。

名稱大小描述
PAGE_N_DlR_SLOTS2位元組頁目錄中的槽數量
PAGE_HEAP_TOP2位元組未使用的空間最小地址,User Records和Free Space分界點
PAGE_N_HEAP2位元組本頁中的記錄的數量(包括虛擬記錄和刪除記錄)
PAGE_FREE2位元組第一個刪除的記錄地址,後續刪除的記錄會形成連結串列。
PAGE_GARBAGE2位元組已刪除記錄佔用的位元組數
PAGE_LAST_INSERT2位元組最後插入記錄的位置
PAGE_DIRECTION2位元組記錄插入的方向
PAGE_N_DIRECTION2位元組同一個方向連續插入的記錄數量
PAGE_N_RECS2位元組該頁中記錄的數量(不包括虛擬記錄和刪除記錄)
PAGE_MAX_TRX_ID8位元組修改當前頁的最大事務ID,僅在二級索引中使用
PAGE_LEVEL2位元組當前頁在B+樹中所處的層級
PAGE_INDEX_ID8位元組索引ID,表示當前頁屬於哪個索引
PAGE_BTR_SEG_LEAF10位元組B+樹葉子段的頭部資訊,僅在B+樹的Root頁定義
PAGE_BTR_SEG_TOP10位元組B+樹非葉子段的頭部資訊,僅在B+樹的Root頁定義

不用每個屬性都瞭解,我們挑幾個比較重要的看看。

PAGE_N_DlR_SLOTS

一個頁內可能有上千條記錄,挨個遍歷的話效率太慢了。為了提高頁內記錄的檢索效率,InnoDB將頁內的記錄劃分為多個組,組裡最大的那條記錄相較於頁的地址偏移量會記錄到「Page Directory」部分,每個組都對應一個槽,槽的大小是固定的2位元組。該屬性記錄的就是頁內槽的數量。

PAGE_HEAP_TOP

Free Space的起始位置,它是User Records和Free Space分界點。一個全新的頁一開始是沒有User Records部分的,每插入一條記錄,都要向Free Space申請空間,Free Space耗盡就代表頁滿了。

PAGE_FREE

DELETE命令刪除記錄時,InnoDB並不會真的將記錄從磁碟中刪除,而是在記錄的頭資訊裡打個標記,然後將其加入到「垃圾連結串列」中。PAGE_FREE指向的就是垃圾連結串列的表頭記錄。後面刪除的記錄,也會自動加入到連結串列裡。

PAGE_DIRECTION & PAGE_N_DIRECTION

PAGE_DIRECTION表示最後一條記錄插入的方向,比上一條記錄值大則記為右邊,反之則是左邊。PAGE_N_DIRECTION表示同一方向連續插入的記錄數,方向變了該值就會重置。

PAGE_LEVEL

InnoDB組織資料的形式就是B+樹,樹中的節點就是索引頁,PAGE_LEVEL代表當前頁在B+樹中所處的層級。InnoDB規定,葉子節點層級為0,然後向上遞增。

2.3 User Records

Infimum和Supremum也屬於記錄,只是為了與使用者記錄區分開才劃分成了兩部分,我們先看User Records。

使用者記錄存放在User Records部分,一個全新的頁一開始全是Free Space,是沒有User Records部分的。每插入一條記錄都需要到Free Space申請一塊空間,並將其劃分到User Records用來存放使用者記錄。當Free Space耗盡也就代表當前頁已經用完了,再有新記錄需要插入,就需要申請一個新的頁了。

還記得MySQL的行格式嗎?它決定了記錄在磁碟裡的儲存格式。以COMPACT為例,儲存格式如下圖:

記錄頭資訊裡的欄位比較關鍵,以防大家忘記,我這裡再貼一下:

名稱大小(Bit)說明
預留位11沒有使用
預留位21沒有使用
deleted_flag1記錄刪除標記
min_rec_flag1B+樹非葉子節點的最小目錄項標記
n_owned4同一頁內同一組裡最大的記錄會記錄組裡的記錄數量,其餘記錄該值為0
heap_no13當前記錄在頁面堆裡的相對位置
record_type3記錄型別。0:普通記錄,1:B+樹非葉子節點目錄項記錄,2:Infimum記錄,3:Supremum記錄.
next_record16下一條記錄的相對位置

記錄頭資訊的最後2位元組用來連線下一條記錄,將頁內所有記錄串聯成一個單向連結串列。所以我們隱藏變長欄位長度列表和NULL值列表,記錄的格式應該是這樣的:

記錄是怎麼排序的?
我們已經知道,頁內的記錄會自動串聯成一個單向連結串列。那這個連結串列的編排順序是什麼呢?是按照記錄的插入時間排序的嗎?其實不是的,如果表有主鍵,會根據主鍵排序;沒主鍵有唯一非空索引,會根據該索引排序;兩者都沒有,InnoDB會自動生成一個row_id列並根據該列進行排序。

若無特殊說明,本文均假定表有主鍵。

2.4 Infimum & Supremum

Infimum和Supremum是索引頁內的兩條虛擬記錄,InnoDB規定所有索引頁都會有這兩條記錄,而且所有的使用者記錄都比Infimum大,都比Supremum小。
記錄頭資訊裡的heap_no代表記錄在堆裡的相對位置,該值越小代表記錄越靠前。細心的同學會發現,上圖中的使用者記錄heap_no值是從2開始的,那0和1呢?不說你也肯定猜到了,就是被Infimum和Supremum佔用了。Infimum和Supremum的heap_no值分別是0和1,它倆在所有使用者記錄的最前面。

Infimum和Supremum結構非常的簡單,和使用者記錄一樣也有頭資訊,真實資料部分是固定的字串,如下圖所示:

我們把這兩條虛擬記錄也加入到記錄裡面,完整的結構就是下面這樣的:

Supremum記錄的next_record屬性為0,代表它已經沒有下一條記錄了。

2.5 Page Directory

Free Space沒什麼好說的,就是一塊未被使用的空閒空間。

Page Directory也叫作「頁目錄」,它的目的是提高頁內記錄的檢索效率。相較於一張表幾千萬的記錄來說,一個頁內幾百上千條記錄已經是很少很少了。可即便如此,它也有幾百上千條啊,如果頁內檢索記錄只能挨個遍歷的話,那也太低效了。別忘了,頁內的記錄是根據索引值排好序的,我們可以巧用「二分法」來快速查詢。

具體做法是:將頁內所有非刪除的記錄劃分為N個組,每個組裡最後一條記錄(即主鍵最大的記錄)稱作“大哥”,其餘記錄是“小弟”,“大哥”的n_owned屬性記錄了組內的記錄數量。將“大哥”在頁內的地址偏移量提取出來,按順序依次從File Trailer部分往前寫,每個地址偏移量佔用2位元組,稱作一個「槽」,Page Directory就是由這些槽構成的。
InnoDB對於分組內的記錄數量有一些規定:

  • Infimum記錄所在分組,只能有一條記錄。
  • Supremum記錄所在分組,允許有1~8條記錄。
  • 其餘分組,允許有4~8條記錄。

由此可見,一個組裡最多有8條記錄,只要通過二分法快速定位到組,InnoDB也只需要遍歷這8條記錄,相較於遍歷頁內所有記錄,效率要高的多。

2.6 File Trailer

File Trailer是所有頁都有的通用結構,佔用固定的8位元組,它的主要作用就是為了校驗頁的完整性。磁碟的速度實在是太慢了,InnoDB不會每次寫點資料都直接重新整理到磁碟上,那樣MySQL會慢死。而是將頁作為刷盤的基本單位,資料修改時,先改記憶體裡的頁,稍後再將整個頁的資料一次性重新整理到磁碟裡。但是這會帶來一個問題,一個頁16KB,刷到第10KB的時候磁碟斷電了怎麼辦?重啟後InnoDB如何判斷磁碟裡的頁資料是完整的?

InnoDB是這麼處理的,刷盤前根據頁資料計算出一個Checksum,在頁頭和頁尾都寫一份。頁刷盤的時候,先刷頁頭再刷頁尾,當頭尾兩個Checksum值一致的時候,代表磁碟裡的頁是完整的,否則就表示頁頭刷了頁尾沒刷,那肯定是刷到一半出錯了。

大小說明
4位元組頁的校驗和Checksum
4位元組頁最後被修改時對應的LSN的後4個位元組,正常情況下應該與File Header裡的FIL_PAGE_LSN的後4個位元組相同。

3. 總結

頁是InnoDB存取資料的基本單位,預設頁大小是16KB,InnoDB為了不同的目的設計了很多不同型別的頁,本文重點分析了存放使用者記錄的索引頁。頁的頭尾部分File Header和File Trailer是所有頁都有的一個通用結構,它們記錄了頁的一些通用狀態資訊,和Checksum用來驗證頁的完整性。Page Header是索引頁特有的結構,它記錄了頁內使用者記錄相關的狀態資訊。User Records部分用來存放使用者記錄。另外,由於頁內的記錄數量也不少,為了提高頁內記錄的檢索效率,InnoDB在索引頁中加入了Page Directory,它通過將記錄分組,將組裡最大的記錄的地址偏移量形成一個個槽,Page Directory就是由這些槽構成的。檢索資料時,使用二分法快速定位到槽所在的組,就可以避免遍歷所有組的記錄了。

到此這篇關於MySQL索引頁結構的文章就介紹到這了,更多相關MySQL索引頁結構內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com