InnoDB從內分析之Page(二)

so_easy發表於2020-09-18

前言

還是從這張圖開始。
InnoDB從內分析(二)

前一分享我已經知道了資料行是如何儲存的,可是不知道是如何在資料頁中儲存的。本章的學習重點就是Page-資料頁了。

Page(資料頁)

首先,我很想知道Page的資料儲存結構:
InnoDB從內分析之Page(二)
那麼怎麼理解這資料結構呢?繼續從上一章的Row開始。
很明顯User Reocrds屬性就是用來儲存使用者的資料行,上一章知道行與行之間是通過單向連結串列儲存。從圖中看到除了我們自己使用者的行記錄之外,還有兩條記錄:Infimum(最小行)Supermum(最大行)。這是InnoDB在建立表時自動生成的,所以一個資料表中最少有兩條記錄。如下圖:
InnoDB從內分析之Page(二)
這個圖的查詢方式類似全表掃描,可是效率低下。於是需要改進,怎麼改進呢?
(1)類似字典可以有目錄查詢,然後快速定位到需要查詢的某頁,在該頁中我們在自己定位到要查的文字。
(2)類似圖書館有每一本書的檔案查詢,可以通過這本書的檔案資訊知道這本書放在了哪個房間、哪個書架,然後在查詢到該書。
如果可以把這些記錄編排一個目錄,然後查詢的時候只需要按這個目錄快速定位就好了。這個目錄就是槽點,多個槽點連在一起就是Slots(目錄槽)。於是得到如下圖:
InnoDB從內分析之Page(二)
可是新的問題就出現了:

  • 槽中應該放多少資料行?
  • 槽應該怎麼取值?

這裡引入InnoDB的規定:

  • Infimum只能包含一條記錄
  • Supermum可以是[1,8]條記錄
  • 其他的則是[4,8]條記錄

槽應該怎麼取值呢?

  • 行分組內最大記錄數的相對位置
  • 注意是相對位置,不是偏移量。

看下別人畫的圖,就是好哇!
InnoDB從內分析之Page(二)

總結以上的幾點:

  • 目錄槽其實就是頁的Page DirectoryPage Directory中就是記錄的所有槽點的集合。
  • 一個槽中的行記錄數就是Row.n_owned,記錄在行組內的最大行內。

File Header和File Trailer

File Header用來記錄頁的頭資訊,如下圖:
InnoDB從內分析之Page(二)
從頭中可以歸納的幾個功能點:

  • 校驗和
  • 屬於哪個表空間及表空間中頁的偏移量
  • 上下頁指標形成頁的連結串列結構
  • 頁的型別
  • 刷髒頁

Page Header

InnoDB從內分析之Page(二)
InnoDB從內分析之Page(二)

  • PAGE_N_RECS(2B):該頁中記錄的數量。2B換算成十進位制那就是65535

Records和Free Space

資料行分為最大行最小行使用者行最大行最小行是在建立資料表的時候就已有的兩條記錄,而使用者行是通過使用者在後續的過程中通過插入資料行新增的資料。如果使用者行的資料被刪除,則空間會被Free Space回收。

Page Directory

頁目錄記錄的是頁相對位置,而不是偏移量。B+樹是通過二分查詢粗略找到該記錄所在的頁,把該頁載入到記憶體,然後通過Page Directory再進行二叉查詢。因二叉查詢速度快再加上在記憶體中,因此這部分的時間經常被忽略。

資料頁結構示例分析

通過以上的概念分析只是有一個大致的瞭解,現在以具體的示例分析。
(1)建立表並寫入資料,分析資料表的二進位制檔案
InnoDB從內分析之Page(二)
InnoDB從內分析之Page(二)
InnoDB從內分析之Page(二)

(2)Page offset 03就是資料頁Page level是0,表示的是根節點。一個Page=16KB,以第一頁起始位置是:0x0000~0x3fff。第三頁的起始就是:0xc000~0ffff

怎麼算呢?
16KB=16*1024KB=16*16*16*4B=0x4000

得到截圖如下:
InnoDB從內分析之Page(二)

結合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)。於是可以得到下圖:
InnoDB從內分析之Page(二)

  • Infimum01 00 02 00 1c表示的是記錄頭資訊5B69 6e 66 69 6d 75 6d 00即表示字元infimum+000x001cRow.next_record,指向的位置是下一個記錄的next_record=0xc062+0x001c=0xc07d
  • Supermum:同理也可以得到表示最大行的header+字元。

分析Page Directory

Page Header中已經知道Page Directory一共有26個槽點,每個槽點佔2B。得到如下圖:
InnoDB從內分析之Page(二)

  • 目錄槽是按逆序存放的,便於從頁尾開始查詢。
  • 0x0063表示的就是最小行Infimum
  • 0x0070 表示的就是最大行Supermum

    如果對於之前槽點記錄的是行分組內最大記錄數的相對位置沒有很直觀的理解的話,在這裡就可以很清楚的瞭解到槽點到底記錄的是什麼了:槽點記錄的是行組內最大行資料列的起始位置。從槽點往前數5B就是記錄頭資訊,從記錄頭資訊n_owned可以得知當前行組內有多少條資料。從槽點往後數就可以得到使用者列的資料,結合分析Row的規則,就可以得到具體某個欄位的值。

以查詢主鍵a=5為例:
通過二分法查詢Page Directory目錄槽,定位到0x00e5,實際位置是0xc0e5。讀取到這行的記錄a=4,不是要查詢的記錄。通過Row.next_record找到下一行記錄則是要找的資料。

InnoDB從內分析之Page(二)

頁分裂

如果一直按照主鍵ID自增的方式插入資料,那麼當一頁寫滿之後在重新分配新的一頁就好。可是如果插入的位置是頁中,而不是頁尾呢?並且碰巧當前的頁已經滿了,寫不下這條新記錄。那麼就會發生頁分裂。一旦發生頁分裂就可能需要耗費大量的時間處理,所以儘量避免這種情況的傳送。

  • 儘量不要從頁中插入資料,儘可能的從頁尾插入資料。
  • 最好建立主鍵並且以自增的方式插入。

回頭看Row格式的header

InnoDB從內分析(一)

B+樹結構初步形成

行與行之間的單向連結串列關係,頁與頁之間的雙向連結串列關係。這已經很容易得出下圖:
InnoDB從內分析之Page(二)

通過頁與頁和行與行之間的關係,我們的確可以找到要查詢的某一條記錄。可是頁與頁之間的查詢效率卻不高。於是還可以優化–將頁號鍵值在提及出來又形成一個。這個頁我們稱之為非葉子節點頁號鍵值則儲存在Row格式中。於是稍作整理得到如下圖:

InnoDB從內分析之Page(二)

隨著記錄的增多,最終得到就是如下圖:
InnoDB從內分析之Page(二)

  • 頁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 協議》,轉載必須註明作者和本文連結
阿門阿前一棵葡萄樹 阿嫩阿嫩綠地剛發芽 蝸牛揹著那重重的殼呀 一步一步地往上爬

相關文章