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

本文基於 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章