系列文章:MySQL系列專欄web
咱們前面簡單提到了頁
的概念,頁
是InnoDB存儲引擎管理數據庫的最小磁盤單位,一個頁的大小通常是16KB
。一次至少讀取一頁的數據到內存,或者刷新一頁的數據到磁盤。數據庫
咱們這節主要來看存放數據記錄的頁,也就是 INDEX 類型的數據頁。markdown
數據頁由 7 個部分組成,大體以下圖所示:性能
其中 File Header、Page Header、File Trailer
的大小是固定的,分別爲 3八、5六、8字節
。User Records、Free Space、Page Directory
這些部分爲實際的行記錄存儲空間,所以大小是動態的。url
前面的文章中簡單提到過記錄頭信息,在介紹後面的內容前,再詳細看下記錄頭信息中存儲了哪些內容。spa
行記錄看下來就像下面這樣:翻譯
delete_mask
這個屬性標記當前記錄是否被刪除,值爲1的時候表示記錄被刪除掉了,值爲0的時候表示記錄沒有被刪除。設計
能夠看出,當刪除一條記錄時,只是標記刪除,實際在頁中尚未被移除。這樣作的主要目的是,之後若是有新紀錄插入表中,能夠複用這些已刪除記錄的存儲空間。3d
min_rec_mask
B+樹的每層非葉子節點
中的最小記錄
都會添加該標記,並設置爲1,不然爲0。日誌
看下圖的索引結構,最底層的葉子節點是存放真實數據的,因此每條記錄的 min_rec_mask 都爲 0。上面兩層是非葉子節點,那麼每一個頁中最左邊的最小記錄的 min_rec_mask 就會設置爲 1。
n_owned
表示當前記錄擁有的記錄數,頁中的數據其實還會分爲多個組,每一個組會有一個最大的記錄,最大記錄的 n_owned 就記錄了這個組中的記錄數。在後面介紹 Page Directory 時會看到這個屬性的用途。
heap_no
這個屬性表示當前記錄在本頁中的位置。
record_type
記錄類型:0 表示普通記錄,1 表示B+樹非葉子節點記錄,2 表示最小記錄,3 表示最大記錄,1xx 表示保留
仍是之前面索引結構圖來看,上面兩層的非葉子節點中的記錄 record_type 都應該爲 1。最底層的葉子節點應該就是普通記錄,record_type 爲 0。其實每一個頁還會有一個最小記錄和最大記錄,record_type 分別爲 2 和 3,這個最小記錄和最大記錄其實就是後面要說的 Infimum 和 Supremum。
next_record
表示從當前記錄的真實數據到下一條記錄的真實數據的地址偏移量,若是沒有下一條記錄就是 0。
數據頁中的記錄看起來就像下圖這樣,按主鍵順序排列後,heap_no
記錄了當前記錄在本頁的位置,而後經過 next_record
鏈接起來。
注意 next_record
指向的是記錄頭與數據之間的位置偏移量。這個位置向左讀取就是記錄頭信息,向右讀取就是真實數據,並且以前說過變長字段長度列表
和NULL值列表
中都是按列逆序存放的,這時往左讀取的標識和往右讀取的列就對應上了,提升了讀取的效率。
若是刪除了其中一條記錄,delete_mask
就設置爲 1,標記爲已刪除,next_record
就會設置爲 0。其實頁中被刪除的記錄會經過 next_record 造成一個垃圾鏈表,供之後插入記錄時重用空間。
File Header 用來記錄頁的一些頭信息,由8個部分組成,固定佔用38字節
。
主要先看下以下的一些信息:
FIL_PAGE_SPACE_OR_CHKSUM
這個表明當前頁面的校驗和(checksum),每當一個頁面在內存中修改了,在同步以前就要把它的校驗和算出來。在一個頁面被刷到磁盤的時候,首先被寫入磁盤的就是這個 checksum。
FIL_PAGE_OFFSET
每個頁都有一個單獨的頁號,InnoDB 經過頁號來惟必定位一個頁。
如某獨立表空間 a.ibd 的大小爲1GB,頁的大小默認爲16KB,那麼總共有65536個頁。FIL_PAGE_OFFSET 表示該頁在全部頁中的位置。若此表空間的ID爲10,那麼搜索頁(10,1)就表示查找表a中的第二個頁。
FIL_PAGE_PREV
和 FIL_PAGE_NEXT
InnoDB 是以頁爲單位存放數據的,InnoDB 表是索引組織的表,數據是按主鍵順序存放的。數據可能會分散到多個不連續的頁中存儲,這時就會經過 FIL_PAGE_PREV 和 FIL_PAGE_NEXT 將上一頁和下一頁連起來,就造成了一個雙向鏈表。這樣就經過一個雙向鏈表把許許多多的頁就都串聯起來了,而無需這些頁在物理上真正連着。
FIL_PAGE_TYPE
這個表明當前頁的類型,InnoDB 爲了避免同的目的而設計了許多種不一樣類型的頁。
InnoDB 有以下的一些頁類型:
Page Header 用來記錄數據頁的狀態信息,由14個部分組成,共佔用56字節
。
PAGE_N_DIR_SLOTS
頁中的記錄會按主鍵順序分爲多個組,每一個組會對應到一個槽(Slot),PAGE_N_DIR_SLOTS
就記錄了 Page Directory 中槽的數量。
PAGE_HEAP_TOP
PAGE_HEAP_TOP 記錄了 Free Space
的地址,這樣就能夠快速從 Free Space 分配空間到 User Records 了。
PAGE_N_HEAP
本頁中的記錄的數量,包括最小記錄(Infimum)和最大記錄(Supremum)以及標記爲刪除(delete_mask=1)的記錄。
PAGE_FREE
已刪除的記錄會經過 next_record
連成一個單鏈表,這個單鏈表中的記錄空間能夠被從新利用,PAGE_FREE 指向第一個標記爲刪除的記錄地址,就是單鏈表的頭節點。
PAGE_GARBAGE
標記爲已刪除的記錄佔用的總字節數。
PAGE_N_RECS
本頁中記錄的數量,不包括最小記錄和最大記錄以及被標記爲刪除的記錄,注意和 PAGE_N_HEAP 的區別。
InnoDB 每一個數據頁中有兩個虛擬的行記錄,用來限定記錄的邊界。Infimum記錄
是比該頁中任何主鍵值都要小的記錄,Supremum記錄
是比改頁中何主鍵值都要大的記錄。這兩個記錄在頁建立時被創建,而且在任何狀況下不會被刪除。
而且因爲這兩條記錄不是咱們本身定義的記錄,因此它們並不存放在頁的User Records
部分,他們被單獨放在一個稱爲Infimum + Supremum
的部分。
Infimum 和 Supremum 都是由5字節
的記錄頭和8字節
的一個固定的部分組成,最小記錄的固定部分就是單詞 infimum
,最大記錄的固定部分就是單詞 supremum
。因爲不存在可變長字段或可爲空的字段,天然就不存在可變長度字段列表和NULL值列表了。
Infimum和Supremum記錄的結構以下圖所示。須要注意,Infimum 記錄頭的 record_type=2
,表示最小記錄;Supremum 記錄頭的 record_type=3
,表示最大記錄。
加上 Infimum 和 Supremum 記錄後,頁中的記錄看起來就像下圖的樣子。Infimum 記錄頭的 next_record 指向該頁主鍵最小的記錄,該頁主鍵最大的記錄的 next_record 則指向 Supremum,Infimum 和 Supremum就構成了記錄的邊界。同時注意,記錄頭中 heap_no
的順序, Infimum 和 Supremum 是排在最前面的。
User Records
就是實際存儲行記錄的部分,Free Space
明顯就是空閒空間。
在一開始生成頁的時候,並無User Records
這個部分,每當插入一條記錄,就會從Free Space
部分中申請一個記錄大小的空間到User Records
部分,當 Free Space 部分的空間用完以後,這個頁也就使用完了。
首先咱們要知道,InnoDB 的數據是索引組織的,B+樹
索引自己並不能找到具體的一條記錄,只能找到該記錄所在的頁,頁是存儲數據的最小基本單位。
以下圖,若是咱們要查找 ID=32 的這行數據,經過索引只能定位到第 17 頁。
定位到頁以後咱們能夠經過最小記錄Infimum
的記錄頭的next_record
沿着鏈表一直日後找,就能夠找到 ID=32 這條記錄了。
可是能夠想象,沿着鏈表順序查找的性能是很低的。因此,頁中的數據實際上是分爲多個組的,這看起來就造成了一個子目錄,經過子目錄就能縮小查詢的範圍,提升查詢性能了。
Page Directory
翻譯過來就是頁目錄,這部分存放的就是一個個的槽(Slot),頁中的記錄分爲了多個組,槽就存放了每一個組中最大的那條記錄的相對位置(記錄在頁中的相對位置,不是偏移量)。這個組有多少條記錄,就經過最大記錄的記錄頭中的 n_owned
來表示。
對於分組中的記錄數是有規定的:Infimum記錄
所在的分組只能有 1 條記錄,Supremum記錄
所在的分組中的記錄條數只能在 1~8
條之間,中間的其它分組中記錄數只能在是 4~8
條之間。
Page Directory
的生成過程以下:
初始狀況下一個數據頁裏只有Infimum
和Supremum
兩條記錄,它們分屬於兩個組。Page Directory 中就有兩個槽,分別指向這兩條記錄,且這兩條記錄的 n_owned
都等於 1
。
以後每插入一條記錄,都會從頁目錄中找到主鍵值比本記錄的主鍵值大而且差值最小的槽
,而後把該槽對應的記錄的 n_owned
值加1
,表示本組內又添加了一條記錄,直到該組中的記錄數等於8條
。
在一個組中的記錄數等於8條
後再插入一條記錄時,會將組中的記錄拆分紅兩個組,一個組中4條
記錄,另外一個5條
記錄。這個過程會在頁目錄中新增一個槽來記錄這個新增分組中最大的那條記錄的相對位置。
當記錄被刪除時,對應槽的最大記錄的 n_owned 會減 1,當 n_owned 小於 4 時,各分組就會平衡一下,總之要知足上面的規定。
其實正常狀況下,按照主鍵自增加新增記錄,可能每次都是添加到 Supremum
所在的組,直到它的 n_owned
等於8
時,再新增記錄時就會分紅兩個組,一個組4條
記錄,一個組5條
記錄。還會新增一個槽,指向4條
記錄分組中的最大記錄,而且這個最大記錄的n_owned
會改成4
,Supremum
的n_owned
就會改成5
。
Page Directory 中槽(Slot)的數量就會記錄到 Page Header
中的 PAGE_N_DIR_SLOTS
。
咱們能夠經過下圖來理解下 Page Directory 中槽(Slot)和分組中最大記錄的關係。
n_owned=1
.n_owned=4
,能夠想象其實就是 Supremum
組分組而來的。Supremum
,這是最大記錄的組,通過分組後,它的 n_owned=5
。能夠看到,頁中的數據通過分組後在 Page Directory 中就造成了一個目錄槽,每一個槽就指向了分組中的最大記錄,最大記錄的記錄頭中的 n_owned
就記錄了這個組中的記錄數。
有了目錄槽以後,InnoDB就會利用二叉查找迅速肯定記錄所在的槽,並找到該槽所在分組中主鍵值最小的那條記錄,再經過最小記錄的 next_record 遍歷記錄,就能快速定位到匹配的那條記錄了。
二叉查找的時間複雜度很低,同時在內存中的查找很快,所以一般會忽略這部分查找所用的時間。
前面介紹 File Header
時說過,在將頁寫入磁盤時,最早寫入的即是 File Header 中的 FIL_PAGE_SPACE_OR_CHKSUM
值,就是頁面的校驗和。在寫入的過程當中,數據庫可能發生宕機,致使頁沒有完整的寫入磁盤。
爲了校驗頁是否完整寫入磁盤,InnoDB 就設置了 File Trailer
部分。File Trailer 中只有一個FIL_PAGE_END_LSN
,佔用8字節
。FIL_PAGE_END_LSN 又分爲兩個部分,前4字節
表明頁的校驗和;後4字節
表明頁面被最後修改時對應的日誌序列位置(LSN),與File Header中的FIL_PAGE_LSN
相同。
默認狀況下,InnoDB存儲引擎每次從磁盤讀取一個頁就會檢測該頁的完整性,這時就會將 File Trailer
中的校驗和、LSN 與 File Header
中的 FIL_PAGE_SPACE_OR_CHKSUM
、FIL_PAGE_LSN
進行比較,以此來保證頁的完整性。