MySQL原理 - InnoDB引擎 - 行記錄儲存 - Off-page 列

乾貨滿滿張雜湊發表於2021-07-07

本文基於 MySQL 8

在前面的兩篇文章,我們分析了 MySQL InnoDB 引擎的兩種行記錄儲存格式:

在這裡簡單總結下:

  • Compact 格式結構:
    • 變長欄位長度表:包括資料不為NULL的每個可變長度欄位的長度,並按照列的順序逆序排列
    • NULL 值列表:針對可以為 NULL 的欄位,用一個 BitMap 來標識哪些欄位為 NULL
    • 記錄頭資訊:固定 5 位元組,包括:
      • 無用位:2 bits,目前沒用
      • deleted_flag:1 bits,標識記錄是否被刪除
      • min_rec_flag:1 bits,是否是 B+ 樹中非葉子節點最小記錄標記
      • n_owned:4 bits,記錄對應的 slot 中擁有的記錄數量
      • heap_no:13 bits,該記錄在堆中的序號,也可以理解為在堆中的位置資訊
      • record_type:3 bits,記錄型別,普通資料記錄為000,節點指標型別為 001,偽記錄首記錄 infimum 行為 010,偽記錄最後一個記錄 supremum 行為 011,1xx 的為保留的
      • next_record 指標:16 bits,頁中下一條記錄的相對位置
    • 隱藏列
      • DB_ROW_ID:6 位元組,這個列不一定會生成。優先使用使用者自定義主鍵作為主鍵,如果使用者沒有定義主鍵,則選取一個 Unique 鍵作為主鍵,如果表中連 Unique 鍵都沒有定義的話,則會為表預設新增一個名為 DB_ROW_ID 的隱藏列作為主鍵
      • DB_TRX_ID:6 位元組,產生當前記錄項的事務 id,每開始一個新的事務時,系統版本號會自動遞增,而事務開始時刻的系統版本號會作為事務 id,事務 commit 的話,就會更新這裡的 DB_TRX_ID
      • DB_ROLL_PTR:7 位元組,undo log 指標,指向當前記錄項的 undo log,找之前版本的資料需通過此指標。如果事務回滾的話,則從 undo Log 中把原始值讀取出來再放到記錄中去
    • 資料列
      • bigint:如果不為 NULL,則佔用8位元組,首位為符號位,剩餘位儲存數字,數字範圍是 -2^63 ~ 2^63 - 1 = -9223372036854775808 ~ 9223372036854775807。如果為 NULL,則不佔用任何儲存空間
      • double:非 NULL 的列,符合 IEEE 754 floating-point "double format" bit layout 這個統一標準,如果為 NULL,則不佔用任何儲存空間
      • 對於定長欄位,不需要存長度資訊直接儲存資料即可如果不足設定的長度則補充。例如 char 型別,補充 0x20, 對應的就是空格。
      • varchar 儲存:因為資料開頭有可變長度欄位長度列表,所以 varchar 只需要儲存實際的資料即可,不需要填充額外的資料。但是我們還沒有考慮儲存特別長資料的情況
  • Redundant 格式結構與 Compact 格式的區別:
    • 所有欄位長度列表:不同於 Compact 行格式,Redundant 的開頭是所有欄位長度列表:記錄所有欄位的長度偏移,包括隱藏列。偏移就是,第一個欄位長度為 a,第二個欄位長度為 b,那麼列表中第一個欄位就是 a,第二個欄位就是 a + b。所有欄位倒序排列
    • 記錄頭資訊:固定 6 位元組
      • 無用位:2 bits,目前沒用
      • deleted_flag:1 bits,標識記錄是否被刪除
      • min_rec_flag:1 bits,是否是 B+ 樹中非葉子節點最小記錄標記
      • n_owned:4 bits,記錄對應的 slot 中擁有的記錄數量
      • heap_no:13 bits,該記錄在堆中的序號,也可以理解為在堆中的位置資訊
      • n_field:10 bits,該記錄的列數量,範圍從1到1023
      • 1byte_offs_flag:1 bit,1 代表每個欄位長度的儲存為 1 位元組,0 代表 2 位元組
      • next_record 指標:16 bits,頁中下一條記錄的相對位置
    • 資料列
      • CHAR 型別儲存:無論欄位是否為 NULL,或者長度是多少,char(M) 都會佔用 M * 位元組編碼最大長度那麼多位元組。為 NULL 的話,填充的是 0x00,不為 NULL,長度不夠的情況下,末尾補充 0x20.

之前並沒有分析當欄位比較長的時候會怎麼儲存,在本篇文章會詳細分析。

在此再回顧下之前提到的。因為每條資料都是一個硬碟定址讀取,我們要減少這個硬碟定址讀取的次數,可以考慮一塊一塊的讀取資料,這樣,我們很可能下次請求需要的資料就已經在記憶體中了,就省去了從硬碟讀取。基於這個思想,InnoDB 將一個表的資料劃分成了若干pages),這些頁通過 B-Tree 索引聯絡起來。每一頁大小預設為 16384 Bytes 也就是 16KB(配置為 innodb_page_size)。

對於比較大的欄位,例如 Text 型別的欄位,如果也存在於這個聚簇索引上,那這個節點資料就會過大,會一下子讀取很多頁出來,這樣讀取效率會降低(例如在我們沒有想讀取這個 Text 列的請求情況下)。所以,InnoDB 對於比較長的變長欄位,一般傾向於將他們儲存在其他地方,這就涉及到了 Off-page 列的設計模式。不同的 行格式 處理不同。

在開始討論不同的 行格式 的處理之前,我們先回顧一下 InnoDB 的頁大小,InnoDB是一個持久化的儲存引擎,也就是資料都是儲存在磁碟上面的。但是讀寫資料,對資料處理,這些是發生在記憶體中。也就是資料需要從磁碟讀取到記憶體。那麼這個讀取是如何讀取呢?如果處理哪條資料,就讀取哪一條到記憶體中,這樣效率也太低了。因為每條資料都是一個硬碟定址讀取,我們要減少這個硬碟定址讀取的次數,可以考慮一塊一塊的讀取資料,這樣,我們很可能下次請求需要的資料就已經在記憶體中了,就省去了從硬碟讀取。基於這個思想,InnoDB 將一個表的資料劃分成了若干頁(pages),這些頁通過 B-Tree 索引聯絡起來。每一頁大小預設為 16384 Bytes 也就是 16KB(配置為 innodb_page_size)。在 MySQL 啟動的時候可以修改,只能是 4096,8192,16384 其中的一個。

Redundant 中 off-page 列處理

對於 Redundant 行格式中比較長的列,只有前 768 位元組會被儲存在資料行上,剩下的資料會被放入其他頁。我們來看一個例項,執行以下 SQL,建立一個測試表,插入測試資料:

drop table if exists long_column_test;
CREATE TABLE `long_column_test` (
`large_content` varchar(32768) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=REDUNDANT;

##長度為 768 位元組
insert into long_column_test values (repeat("az", 384));
##長度為 8100 位元組
insert into long_column_test values (repeat("az", 4050));
##長度為 32768 位元組
insert into long_column_test values (repeat("az", 16384));

我們使用 64 進位制編碼器檢視錶檔案 long_column_test.ibd,可以看到第一條資料是一條正常的資料,其儲存和之前我們講的 Redundant 列儲存一樣,沒有特殊的:

image

所有欄位長度列表(8位元組,4列,一個資料列,三個隱藏列):03 13(768+7+6+6),00 13(7+6+6),00 0c(6+6), 00 06(6)
記錄頭(6位元組):00 00 10 08 03 ac
隱藏列 DB_ROW_ID(6位元組):00 00 00 00 02 22 
隱藏列 DB_TRX_ID(6位元組):00 00 00 00 58 b7
隱藏列 DB_ROLL_PTR(7位元組):82 00 00 01 0c 01 10 
資料列 large_content(768位元組):61 7a ......

對於第二行,我們發現這一行的 large_content 列的資料並沒有完全儲存在這一行,而是一部分儲存在這一行,另一部分儲存在了其他地方,這種列就被稱為 off-page 列,儲存到的其他地方被稱為 overflow 頁,其結構如下:
image

首先是資料列

所有欄位長度列表(8位元組,4列,一個資料列,三個隱藏列):43 27(第一位元組的頭兩位不代表長度,最高位還是標記欄位是否為NULL,第二位標記這條記錄是否在同一頁,由於不為 NULL,所以最高位為 0,由於存在 overflow 頁所以不在同一頁,所以第二位為1,後面的 3 27 代表長度,即 20+768+7+6+6),00 13(7+6+6),00 0c(6+6), 00 06(6)
記錄頭(6位元組):00 00 10 08 03 ac
隱藏列 DB_ROW_ID(6位元組):00 00 00 00 02 22 
隱藏列 DB_TRX_ID(6位元組):00 00 00 00 58 b7
隱藏列 DB_ROLL_PTR(7位元組):82 00 00 01 0c 01 10 
資料列 large_content(768位元組):61 7a ......
指向剩餘資料所在地址的指標(20位元組):00 00 05 23 00 00 00 05 00 00 00 01 00 00 00 00 00 00 1c a4

對於 off-page 列,列資料末尾會存在指向剩餘資料所在地址的指標,這個指標佔用 20 位元組,它的結構是:

image

然後是列剩下的資料儲存到的 overflow 頁

資料列 large_content(剩餘的 7332 位元組):61 7a ......

當欄位再長一些呢,超過一頁內資料的限制的時候呢?我們來看第三行資料結構:

image

可以看出,過長的資料列,會以連結串列連結的形式儲存在 overflow 頁上。

由此可見 Redundant 行格式中,off-page 的結構其實是:
image

這樣我們會聯想到三個問題:

  1. 什麼時候列會變成 off-page 列?
  2. 什麼時候 overflow 頁會分成一個個連結串列節點儲存?
  3. 對於哪些列型別會這麼儲存?

1. 什麼時候列會變成 off-page 列?

首先我們知道一點,innodb 引擎的頁大小預設是 16KB,也就是 16384 位元組,而且 innodb 的資料是按頁載入的。然後,組織 innoDB 引擎資料的資料結構是 B+ 樹。掃描 B+ 樹尋找資料,也是一頁一頁載入搜尋的。如果一頁內能包含的資料行越多,那麼很明顯,搜尋效率越高。但是如果一頁中只有一條資料,那麼這個 B+ 樹其實和連結串列的效率差不多了。所以,為了效率,需要保證一頁內至少有兩條資料。所以有:

\[2 * 行資料大小 \lt 16384 \rightarrow 行資料大小 \lt 8192 \]

同時,一行資料並不是只有列資料,還有隱藏列,記錄頭,列長度列表等等,並且,innoDB 頁也有自己的一些後設資料(佔用 132 位元組,我們在以後的章節會詳細分析),在這裡我們拿 long_column_test 作為例子,則有:

\[page 後設資料大小 + 2 * `long\_column\_test` 行資料大小 \lt 16384 \rightarrow 132 + 2 * (欄位長度列表長度 + 記錄頭長度 + 三個隱藏列長度 + large\_content 長度) \lt 16384 \]

可以推匯出:

\[large\_content 長度 \lt 8093 \]

在實際使用中,可能不止一列資料比較長。還有,由於資料不儲存在行資料一起,搜尋讀取效率會比較低,所以,redundant 行格式會盡可能不把列變為 off-page 列,並儘量少的將列變為 off-page 列。

2. 什麼時候 overflow 頁會分成一個個連結串列節點儲存?

overflow 頁和表資料不同,不通過 B+ 樹組織資料,同時不會做複雜搜尋,它就是一個連結串列。所以我們只要保證資料大小不超過一頁即可,即:

\[overflow 頁資料節點大小 \lt 16384 \]

這個資料節點也是有一些額外資訊的,同時,頁也是有自己的額外資訊的,這些會在之後的文章中看到。所以,真正承載的資料大小,會需要刨除這些額外資訊,也就是小於 16384。如果不夠,就會分成多頁儲存,這些節點會通過一個連結串列連結起來。

3. 對於哪些列型別會這麼儲存?

對於可變長度欄位,例如 varchar,varbinary,text,blob 等,會利用這種機制儲存。對於定長欄位,例如 char,如果超長,也會像 varchar 一樣儲存,在這種情況下,char 末尾就不會填充空白字元了。但是這種情況不常見,char 最長只能 255 個字元,字元編碼必須是大於三位元組的時候,才會大於 768,例如 uf8mb4 並且每個字元都是大於 3 位元組的字元。

Compact 中 off-page 列處理

Compact 中對於 off-page 的處理與 Redundant 基本一樣,只是由於資料結構不一樣:
image

導致列會變成 off-page 列的臨界點不一樣,在這裡我們拿 long_column_test 作為例子,則有:

\[page 後設資料大小 + 2 * `long\_column\_test` 行資料大小 \lt 16384 \rightarrow 132 + 2 * (變長長度列表 2 位元組 + NULL 值列表 1 位元組 + 記錄頭長度 5 位元組 + 三個隱藏列長度(6+6+7 位元組) + large\_content 長度) \lt 16384 \]

可以推匯出:

\[large\_content 長度 \lt 8099 \]

Dynamic 中 off-page 列處理

Dynamic 除了 off-page 列處理和 Compact 不同以外,其他的基本和 Compact 一樣

Dynamic 對於 off-page 列處理的主要區別在於,所有的資料都儲存在 overflow 頁上面,在 off-page 列只儲存 20 位元組指標,這個指標的結構和 Redundant 格式中的 20 位元組指標一樣:
image

Compressed 中 off-page 列處理

Compressed 行格式和 Dynamic 基本一致,包括對於 off-page 列處理,其實就是在 Dynamic 的基礎上,增加了壓縮處理。對於壓縮處理,會在後面的壓縮頁章節詳細分析。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer
image

相關文章