本文基於 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 列存儲一樣,沒有特殊的:
所有字段長度列表(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 頁,其結構如下:
首先是數據列
所有字段長度列表(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 字節,它的結構是:
然后是列剩下的數據存儲到的 overflow 頁:
數據列 large_content(剩余的 7332 字節):61 7a ......
當字段再長一些呢,超過一頁內數據的限制的時候呢?我們來看第三行數據結構:
可以看出,過長的數據列,會以鏈表鏈接的形式存儲在 overflow 頁上。
由此可見 Redundant 行格式中,off-page 的結構其實是:
這樣我們會聯想到三個問題:
- 什么時候列會變成 off-page 列?
- 什么時候 overflow 頁會分成一個個鏈表節點存儲?
- 對於哪些列類型會這么存儲?
1. 什么時候列會變成 off-page 列?
首先我們知道一點,innodb 引擎的頁大小默認是 16KB,也就是 16384 字節,而且 innodb 的數據是按頁加載的。然后,組織 innoDB 引擎數據的數據結構是 B+ 樹。掃描 B+ 樹尋找數據,也是一頁一頁加載搜索的。如果一頁內能包含的數據行越多,那么很明顯,搜索效率越高。但是如果一頁中只有一條數據,那么這個 B+ 樹其實和鏈表的效率差不多了。所以,為了效率,需要保證一頁內至少有兩條數據。所以有:
同時,一行數據並不是只有列數據,還有隱藏列,記錄頭,列長度列表等等,並且,innoDB 頁也有自己的一些元數據(占用 132 字節,我們在以后的章節會詳細分析),在這里我們拿 long_column_test
作為例子,則有:
可以推導出:
在實際使用中,可能不止一列數據比較長。還有,由於數據不存儲在行數據一起,搜索讀取效率會比較低,所以,redundant 行格式會盡可能不把列變為 off-page 列,並盡量少的將列變為 off-page 列。
2. 什么時候 overflow 頁會分成一個個鏈表節點存儲?
overflow 頁和表數據不同,不通過 B+ 樹組織數據,同時不會做復雜搜索,它就是一個鏈表。所以我們只要保證數據大小不超過一頁即可,即:
這個數據節點也是有一些額外信息的,同時,頁也是有自己的額外信息的,這些會在之后的文章中看到。所以,真正承載的數據大小,會需要刨除這些額外信息,也就是小於 16384。如果不夠,就會分成多頁存儲,這些節點會通過一個鏈表鏈接起來。
3. 對於哪些列類型會這么存儲?
對於可變長度字段,例如 varchar,varbinary,text,blob 等,會利用這種機制存儲。對於定長字段,例如 char,如果超長,也會像 varchar 一樣存儲,在這種情況下,char 末尾就不會填充空白字符了。但是這種情況不常見,char 最長只能 255 個字符,字符編碼必須是大於三字節的時候,才會大於 768,例如 uf8mb4 並且每個字符都是大於 3 字節的字符。
Compact 中 off-page 列處理
Compact 中對於 off-page 的處理與 Redundant 基本一樣,只是由於數據結構不一樣:
導致列會變成 off-page 列的臨界點不一樣,在這里我們拿 long_column_test
作為例子,則有:
可以推導出:
Dynamic 中 off-page 列處理
Dynamic 除了 off-page 列處理和 Compact 不同以外,其他的基本和 Compact 一樣。
Dynamic 對於 off-page 列處理的主要區別在於,所有的數據都存儲在 overflow 頁上面,在 off-page 列只存儲 20 字節指針,這個指針的結構和 Redundant 格式中的 20 字節指針一樣:
Compressed 中 off-page 列處理
Compressed 行格式和 Dynamic 基本一致,包括對於 off-page 列處理,其實就是在 Dynamic 的基礎上,增加了壓縮處理。對於壓縮處理,會在后面的壓縮頁章節詳細分析。
微信搜索“我的編程喵”關注公眾號,每日一刷,輕松提升技術,斬獲各種offer