前言
還是從這張圖開始。
前一分享我已經知道了資料行
是如何儲存的,可是不知道是如何在資料頁
中儲存的。本章的學習重點就是Page
-資料頁了。
Page(資料頁)
首先,我很想知道Page
的資料儲存結構:
那麼怎麼理解這資料結構呢?繼續從上一章的Row
開始。
很明顯User Reocrds
屬性就是用來儲存使用者的資料行,上一章知道行與行之間是通過單向連結串列儲存。從圖中看到除了我們自己使用者的行記錄之外,還有兩條記錄:Infimum(最小行)
和Supermum(最大行)
。這是InnoDB在建立表時自動生成的,所以一個資料表中最少有兩條記錄。如下圖:
這個圖的查詢方式類似全表掃描,可是效率低下。於是需要改進,怎麼改進呢?
(1)類似字典可以有目錄查詢,然後快速定位到需要查詢的某頁,在該頁中我們在自己定位到要查的文字。
(2)類似圖書館有每一本書的檔案查詢,可以通過這本書的檔案資訊知道這本書放在了哪個房間、哪個書架,然後在查詢到該書。
如果可以把這些記錄編排一個目錄,然後查詢的時候只需要按這個目錄快速定位就好了。這個目錄就是槽點
,多個槽點
連在一起就是Slots(目錄槽)
。於是得到如下圖:
可是新的問題就出現了:
- 槽中應該放多少資料行?
- 槽應該怎麼取值?
這裡引入InnoDB的規定:
Infimum
只能包含一條記錄Supermum
可以是[1,8]條記錄- 其他的則是[4,8]條記錄
槽應該怎麼取值呢?
- 行分組內最大記錄數的相對位置
- 注意是相對位置,不是偏移量。
看下別人畫的圖,就是好哇!
總結以上的幾點:
目錄槽
其實就是頁的Page Directory
。Page Directory
中就是記錄的所有槽點的集合。- 一個槽中的行記錄數就是
Row.n_owned
,記錄在行組內的最大行內。
File Header和File Trailer
File Header
用來記錄頁的頭資訊,如下圖:
從頭中可以歸納的幾個功能點:
- 校驗和
- 屬於哪個表空間及表空間中頁的偏移量
- 上下頁指標形成頁的連結串列結構
- 頁的型別
- 刷髒頁
Page Header
- PAGE_N_RECS(2B):該頁中記錄的數量。
2B
換算成十進位制那就是65535
Records和Free Space
資料行
分為最大行
、最小行
和使用者行
。最大行
和最小行
是在建立資料表的時候就已有的兩條記錄,而使用者行
是通過使用者在後續的過程中通過插入資料行新增的資料。如果使用者行
的資料被刪除,則空間會被Free Space
回收。
Page Directory
頁目錄
記錄的是頁相對位置,而不是偏移量
。B+樹是通過二分查詢粗略找到該記錄所在的頁,把該頁載入到記憶體,然後通過Page Directory
再進行二叉查詢。因二叉查詢速度快再加上在記憶體中,因此這部分的時間經常被忽略。
資料頁結構示例分析
通過以上的概念分析只是有一個大致的瞭解,現在以具體的示例分析。
(1)建立表並寫入資料,分析資料表的二進位制檔案
(2)Page offset 03
就是資料頁
。Page level
是0,表示的是根節點。一個Page=16KB
,以第一頁起始位置是:0x0000
~0x3fff
。第三頁的起始就是:0xc000
~0ffff
。
怎麼算呢?
16KB=16*1024KB=16*16*16*4B=0x4000
得到截圖如下:
結合Page
的組成記憶體佔用情況:
File Header:38B
Page Header:56B
File Trailer:8B
那麼已經可以確定這三個結構的位置。
(1)File Header:0xc000+38B-1 = 0xc025。即0xc000~0xc025
(2)Page Header:0xc026+56B-1= 0xc05d。即0xc026~0xc05d
(3)File Trailer:最後8個位元組。
分析Page Header
PAGE_N_DIR_SLOTS
(2B):0x001a=26。說明有26個槽點,每個槽點佔用2B,所以可以從File Trailer
往前數52B就是Page Directory
的內容了。PAGE_HEAP_TOP
(2B):表示堆第一個記錄的指標。起始位置為0xc000+0x0dc0=0xcdc0。PAGE_N_HEAP
(2B):當行格式為Compact時初始值為0x0802
,行格式為Redundant是起始值是2
。0x8066-0x0802=0x64,所以有100
條記錄。PAGE_FREE
(2B):指向可重用空間的首指標,值為0x00。因為沒有刪除,所以為0。PAGE_GARBAGE
(2B):已刪除記錄的位元組數,沒有刪除所以為0x00。PAGE_LAST_INSERT
(2B):最後插入記錄的位置,值為:0xc000+0x0da5=0xcda5。PAGE_DIRECTION
(2B):最後插入方向,值為0x0002。因為一直在插入資料,所以最終是向右插入。PAGE_N_DIRECTION
(2B):一個方向連續插入的數量。值為0x0063,即往右連續插入了99條記錄。PAGE_N_RECS
(2B):該頁中記錄的數量。值為0x0064,即100條記錄。PAGE_LEVEL
:值為0x00,代表該頁為葉子節點。目前記錄行數較少,B+樹只有一層。B+樹葉子層總是0x00
。PAGE_INDEX_ID
(8B):索引ID,表示當前頁屬於哪個索引。
Infimum和Supermum
最小行和最大行緊跟著Page Header
。最小行和最大行的行格式和使用者行的行格式結構是一樣的,只不過它們只有記錄頭資訊
和一個欄位char(8)
。於是可以得到下圖:
Infimum
:01 00 02 00 1c
表示的是記錄頭資訊
的5B
。69 6e 66 69 6d 75 6d 00
即表示字元infimum
+00
。0x001c
是Row.next_record
,指向的位置是下一個記錄的next_record=0xc062+0x001c=0xc07d
。Supermum
:同理也可以得到表示最大行的header
+字元。
分析Page Directory
從Page Header
中已經知道Page Directory
一共有26個槽點,每個槽點佔2B。得到如下圖:
目錄槽
是按逆序存放的,便於從頁尾開始查詢。0x0063
表示的就是最小行Infimum
。0x0070
表示的就是最大行Supermum
。如果對於之前槽點記錄的是
行分組內最大記錄數的相對位置
沒有很直觀的理解的話,在這裡就可以很清楚的瞭解到槽點
到底記錄的是什麼了:槽點
記錄的是行組內最大行資料列的起始位置
。從槽點
往前數5B
就是記錄頭資訊
,從記錄頭資訊
的n_owned
可以得知當前行組內有多少條資料。從槽點
往後數就可以得到使用者列
的資料,結合分析Row
的規則,就可以得到具體某個欄位的值。
以查詢主鍵a=5
為例:
通過二分法查詢Page Directory
目錄槽,定位到0x00e5
,實際位置是0xc0e5
。讀取到這行的記錄a=4
,不是要查詢的記錄。通過Row.next_record
找到下一行記錄則是要找的資料。
頁分裂
如果一直按照主鍵ID自增的方式插入資料,那麼當一頁寫滿之後在重新分配新的一頁就好。可是如果插入的位置是頁中,而不是頁尾呢?並且碰巧當前的頁已經滿了,寫不下這條新記錄。那麼就會發生頁分裂
。一旦發生頁分裂
就可能需要耗費大量的時間處理,所以儘量避免這種情況的傳送。
- 儘量不要從頁中插入資料,儘可能的從頁尾插入資料。
- 最好建立主鍵並且以自增的方式插入。
回頭看Row格式的header
B+樹結構初步形成
行與行之間的單向連結串列關係,頁與頁之間的雙向連結串列關係。這已經很容易得出下圖:
通過頁與頁和行與行之間的關係,我們的確可以找到要查詢的某一條記錄。可是頁與頁之間的查詢效率卻不高。於是還可以優化–將頁號
和鍵值
在提及出來又形成一個頁
。這個頁我們稱之為非葉子節點
。頁號
和鍵值
則儲存在Row
格式中。於是稍作整理得到如下圖:
隨著記錄的增多,最終得到就是如下圖:
頁10
:表示的是FIL_PAGE_OFFSET
的值。- 橙色的值1、3、4表示的是主鍵
- 頁10下的第一行
2 0 0 0 3
表示的是Row.record_type
。
更多可參考:juejin.im/post/6844903582550982670
其他知識點
葉子節點:包含行資料和索引的
Page
,稱為葉子節點。非葉子節點:只包含索引欄位的
Page
,則是非葉子節點。
葉子節點和非葉子節點除了都是B+樹的葉子節點關係外,葉子節點會形成一個連結串列,而非葉子節點也會形成一個連結串列。聚簇索引:包含行資料和索引的B+樹。定義了主鍵的資料表會建立以主鍵為索引的聚簇索引,如果沒有定義主鍵則以唯一鍵為聚簇索引,否則使用隱藏
RowID
為聚簇索引。非聚簇索引:只包含主鍵和其他索引的B+樹。如唯一索引、聯合索引、普通索引都是。如果查詢的列在非聚簇索引不存在,那麼就會涉及到
回表查詢
。回表查詢
則會返回聚簇索引按主鍵去查詢列值。
參考
1、mysql儲存引擎InnoDB詳解,從底層看清InnoDB資料結構
2、MySQL 技術內幕(InnoDB 儲存引擎)第二版
本作品採用《CC 協議》,轉載必須註明作者和本文連結