行格式決定了我們插入的一行數據如何存儲在數據庫中。MySQL有四種行格式,分別是冗余、緊湊、動態和壓縮。
不同線條格式的差異:
行格式
緊湊內存
增強型可變長度列存儲
大索引鍵前綴
壓縮支持
支持的表空間類型
要求的文件格式
多余的
不
不
不
不
系統,每表文件,常規
羚羊或梭魚
緊密的
是
不
不
不
系統,每表文件,常規
羚羊或梭魚
動態的
是
是
是
不
系統,每表文件,常規
梭魚
壓縮的
是
是
是
是
每表文件,常規
梭魚
MySQL 5.7默認使用動態行格式。
我們可以在創建表格時指定字符集和行格式。
字符集表示我們插入的字符是用幾個字節編碼的,比如一個字節用于ASCII,兩個字節用于GB2312,三個字節用于utf8,四個字節用于utf8mb4(如果存儲了emoj表達式,應該使用這個字符集)
CREATETABLE`test `(
` id ` int(11)NOTNULLAUTO _ INCREMENT,
` price`int(11)NOTNULL,
` code`int(11)NOTNULL,
主鍵(` id `)
)ENGINE=InnoDBDEFAULTCHARSET=ut F8 MB 4 row _ FORMAT=Dynamic;
當談到下面的行格式時,我們用這個表來解釋。
CREATETABLErecord_format_demo(
c1VARCHAR(10),
c2VARCHAR(10)NOTNULL,
c3CHAR(10),
c4VARCHAR(10)
)CHARSET=ascii row _ FORMAT=Redundant;
插入記錄格式演示(c1,c2,c3,c4)值
(' aaaa ',' bbb ',' cc ',' d '),
(' eeee ',' fff ',NULL,NULL);
冗余行格式
冗余行格式是一種老的行格式,現在只有部分MySQL系統表可以使用。通常情況下,我們不使用它,但我認為這種行格式是理解其他行格式的基礎。
行格式結構
- 字段長度偏移列表
在Redundant行格式中,會把所有字段的真實數據占?的字節長度都存放在記錄的開頭部位,從?形成?個字段長度偏移列表,字段長度占?的字節數按照列的順序逆序存放,逆序存放,逆序存放!
- 記錄頭信息Redundant?格式的記錄頭信息占?6字節,48個?進制位,這些?進制位代表的意思如下
名稱 | 大小(bit) | 描述 |
---|---|---|
預留位1 | 1 | 未使用 |
預留位2 | 1 | 未使用 |
delete_mask | 1 | 該記錄是否刪除 |
min_rec_mask | 1 | B+樹每層非葉子節點最小記錄都會添加該標記 |
n_owned | 4 | 當前記錄組擁有記錄數 |
heap_no | 13 | 當前記錄在頁面堆位置信息 |
n_field | 10 | 記錄中列數量 |
1byte_offs_flag | 1 | 字段長度偏移列表中每個列對應的偏移量是使?1字節還是2字節表?的 |
next_record | 16 | 下一條記錄的相對位置 |
- 真實數據
對于record_format_demo表來說,記錄的真實數據除了c1、c2、c3、c4這?個我們??定義的列的數據以外,MySQL會為每個記錄默認的添加?些列(也稱為隱藏列),具體的列如下
- DB_ROW_ID(row_id) : 當表沒有定義主鍵,則選擇unique鍵作為主鍵,如果仍沒有,則默認添加一個名為DB_ROW_ID的隱藏列作為主鍵,占用6個字節。也就是說這個列只有當沒有主鍵也沒有唯一索引時才存在
- DB_TRX_ID(transaction_id): 事務id,占用6字節
- DB_ROLL_PTR(roll_pointer): 占用7個字節,回滾指針(后面MVCC的時候會用到)
所以,對我們的數據來講,其行格式數據如下
列長度如何計算
?如第?條記錄的字段?度偏移列表就是:
25 24 1A 17 13 0C 06
因為它是逆序排放的,所以按照列的順序排列就是:
06 0C 13 17 1A 24 25
計算各個列長度則按照以下方法(字符集是ascii,一個字符占用1個字節):第?列(row_id
)的?度就是 0x06個字節,也就是6個字節。
第?列(transaction_id
)的?度就是 (0x0C - 0x06)個字節,也就是6個字節。
第三列(roll_pointer
)的?度就是 (0x13 - 0x0C)個字節,也就是7個字節。第四列(c1
)的?度就是 (0x17 - 0x13)個字節,也就是4個字節。
第五列(c2
)的?度就是 (0x1A - 0x17)個字節,也就是3個字節。
第六列(c3
)的?度就是 (0x24 - 0x1A)個字節,也就是10個字節。
第七列(c4
)的?度就是 (0x25 - 0x24)個字節,也就是1個字節。
在記錄頭信息中的1byte_offs_flag用于表示 字段長度偏移列表中每個列對應的偏移量是使?1字節還是2字節表?的 ,這個值是如何計算的呢?
- 當記錄的真實數據占?的字節數不?于127(?六進制0x7F,?進制01111111)時,每個列對應的偏移量占?1個字節。
- 當記錄的真實數據占?的字節數?于127,但不?于32767(?六進制0x7FFF,?進制0111111111111111)時,每個列對應的偏移量占?2個字節。
- 當記錄大于32767的時候,此時的記錄已經存放到了溢出頁中,在本頁中只保留前768個字節和20個字節的溢出頁?地址(當然這20個字節中還記錄了?些別的信息)。因為字段?度偏移列表處只需要記錄每個列在本頁?中的偏移就好了,所以每個列使?2個字節來存儲偏移量就夠了。
我們的第一條記錄真實數據總長度 = 37(6+6+7+4+3+10+1),小于127,所以采用1字節記錄偏移量。
為了在解析記錄的時候知道列偏移量是采用1字節還是2字節表示,因此使用1byte_offs_flag來決定,當它的值為1時,表明使用1個字節存儲,當值為0時,表明使用2字節存儲。
需要注意下記錄頭信息的next_record,你可以把它理解為指針,通過它我們可以指向下一條記錄的位置(多條記錄是如何連接的會在下一篇文章講到哈),當我們指針在這個位置的時候往后讀是真實數據的位置,往前讀就是字段的長度列表,所以我們長度列表逆序存放就能和真實數據一一對應。

Redundant行格式對NULL值的處理
列對應偏移量值的第一個比特位作為列值是否為NULL的依據,當解析一條記錄某個列時,首先查看這個比特位的值是否為1,如果是1,那么該列的值就是NULL,否則則不是NULL。(現在你知道為什么記錄數據長度為什么會有127和32767這兩個臨界點了吧)
這個bit位也可以稱為NULL比特位
對于值為NULL的列,如果是定長類型,NULL值也將占用記錄的真實數據部分,數據采用0x00字節填充。如果是變長數據類型,則不在記錄的真實數據處占用任何存儲空間。
如上圖我們的第二條數據, C3列的值是NULL,類型是CHAR(10),占?記錄的真實數據部分10字節(,所以我們看到在Redundant?格式中使?0x00000000000000000000來表?NULL值。
C3列長度偏移量是0xA4,二進制是 10100100,最高位是1,表明該列值是NULL,將高位去掉變成 0100100(十進制的36), C2列對應偏移量是0x1A(十進制的26),因此其長度是36-26=10
C4列是Varchar類型,對應偏移量是0xA4,C3列偏移量也是0XA4,表明其長度是0(不占用真實數據存儲空間),而其二進制高位是1,表明該列值是NULL。
為什么定長類型NULL值也要占用固定空間呢?官方文檔告訴我對于一個固定長度的列,該列的固定長度被保留在記錄的數據部分。為NULL值保留的固定空間允許列從NULL值更新到非NULL值,而不會引起索引頁的碎片化。
Compact行格式
Compact行格式是Dynamic和Compressed兩種行格式的基礎,了解了它就了解了其他兩種結構
行格式結構

如上圖,Compact行格式中記錄額外信息分為變長字段長度列表,NULL值列表,記錄頭信息。
變長字段列表中存儲的是非空的變長字段的數據長度,變長字段存儲的數據是不固定的,所以我們需要將數據占用的字節數也存起來。同樣的,這里占用的長度也是逆序存放,逆序存放,逆序存放的。
varchar(M),VARBINARY(M),各種TEXT以及各種BLOB類型,mysql把擁有這些數據類型的列稱為變長字段
對NULL值的處理
Redundant是將列對應偏移量值的第一個比特位作為列值是否為NULL的依據,但是在Compact中是單獨有一個NULL值列表來存儲值為NULL的字段。NULL值列表是如何確認的呢?
- 首先統計表接口中允許為NULL值的列(主鍵和unique key是不允許為NULL的)
- 如果表中沒有允許存儲 NULL 的列,則 NULL值列表 也不存在了,否則將每個允許存儲NULL的列對應?個?進制位,?進制位按照列的順序逆序排列,逆序排列,逆序排列
- ?進制位的值為1時,代表該列的值為NULL。
- ?進制位的值為0時,代表該列的值不為NULL。
- MySQL規定NULL值列表必須?整數個字節的位表?,如果使?的?進制位個數不是整數個字節,則在字節的?位補0。
如果一個表中有9個允許為NULL的列,那么就需要用2個字節表示
對于我們上面的兩條數據來說(c1,c3,c4允許為NULL)
('aaaa', 'bbb', 'cc', 'd'),
('eeee', 'fff', NULL, NULL);
第一條數據NULL值列表為 00000000(都不為空)第二條數據NULL值列表為 00000110,c1不為null,所以是0,c3為null,所以是1,c4是null,所以是1,其倒序結果就是00000110
記錄頭
和Redundant不同,Compact的記錄頭信息使用了5個字節(40bit)來表示記錄頭信息,其具體信息如下
名稱 | 大小(bit) | 描述 |
---|---|---|
預留位1 | 1 | 未使用 |
預留位2 | 1 | 未使用 |
delete_mask | 1 | 該記錄是否刪除 |
min_rec_mask | 1 | B+樹每層非葉子節點最小記錄都會添加該標記 |
n_owned | 4 | 當前記錄組擁有記錄數 |
heap_no | 13 | 當前記錄在頁面堆位置信息 |
record_type | 3 | 表?當前記錄的類型,0表?普通記錄,1表?B+樹?葉?節點記錄,2表?最?記錄,3表?最?記錄 |
next_record | 16 | 下一條記錄的相對位置 |
可以看到相比Redundant,Compact多了一個record_type的字段,少了n_field和1byte_offs_flag兩個字段。
我們之前提到過 1byte_offs_flag 是用來表示 字段長度偏移列表中每個列對應的偏移量是使?1字節還是2字節表?的, 但是Compact卻沒有,那變長字段長度列表中字段長度到底是用1個字節表示還是2個字節表示呢?
列長度如何計算
還記得Redundant將列對應偏移量值的第一個比特位作為列值是否為NULL的依據嗎?Compact思路也是類似的,它使用字節的第一位來表示.
- 假設某個字符集中表??個字符最多需要使?的字節數為W,也就是使?SHOW CHARSET語句的結果中的Maxlen列,??說utf8字符集中的W就是3,gbk字符集中的W就是2,ascii字符集中的W就是1。
- 對于變長類型VARCHAR(M)來說,這種類型表?能存儲最多M個字符(注意是字符不是字節),所以這個類型能表?的字符串最多占?的字節數就是M×W。
- 假設它實際存儲的字符串占?的字節數是L。所以確定使?1個字節還是2個字節表?真正字符串占?的字節數的規則就是這樣:
- 如果M×W <= 255,那么使?1個字節來表?真正字符串占?的字節數。也就是說InnoDB在讀記錄的變長字段長度列表時先查看表結構,如果某個變長字段允許存儲的最?字節數不?于255時,可以認為只使?1個字節來表?真正字符串占?的字節數。
- 如果M×W > 255,則分為兩種情況:
- 如果L <= 127,則?1個字節來表?真正字符串占?的字節數。
- 如果L > 127,則?2個字節來表?真正字符串占?的字節數。InnoDB在讀記錄的變長字段長度列表時先查看表結構,如果某個變長字段允許存儲的最?字節數?于255時,該怎么區分它正在讀的某個字節是?個單獨的字段長度還是半個字段長度 呢?該字節的第?個?進制位作為標志位:如果該字節的第?個位為0,那該字節就是?個單獨的字段長度(使??個字節表?不?于127(01111111)的?進制的第?個位都 為0),如果該字節的第?個位為1,那該字節就是半個字段長度。對于?些占?字節數?常多的字段,??說某個字段長度?于了16KB,那么如果該記錄在單個頁?中?法存儲 時,InnoDB會把?部分數據存放到所謂的溢出頁中,在變長字段長度列表處只存儲留在本頁?中的長度,所以使?兩個字節也可以存放下來??偨Y?下就是說:如果該可變字段允許存儲的最?字節數(M×W)超過255字節并且真實存儲的字節數(L)超過127字節,則使?2個字節,否則使?1個字節。
上面的內容參考了小孩子大佬的<<MySQL是怎樣運行的:從根兒上理解MYSQL>>,大家可以在掘金購買它的小冊或者對應的實體書。他是從Compact講到Redundant的,但是我覺得從Redundant的格式到Compact格式其實更容易理解,過度更容易。這也是我的一個理解,供大家參考。
行溢出
在Compact和Reduntant?格式中,對于占?存儲空間?常?的列,在記錄的真實數據處只會存儲該列的?部分數據,把剩余的數據分散存儲在?個其他的頁中,然后記錄的真實數據處?20個字節存儲指向這些頁的地址(當然這20個字節中還包括這些分散在其他頁?中的數據的占?的字節數),從?可以找到剩余數據所在的頁。
對于Compact和Reduntant?格式來說,如果某?列中的數據?常多的話,在本記錄的真實數據處只會存儲該列的前768個字節的數據和?個指向其他頁的地址(如果一個頁都放不下,那么就會使用鏈表將多個頁鏈接起來),然后把剩下的數據存放 到其他頁中,這個過程也叫做?溢出,存儲超出768字節的那些頁?也被稱為溢出?。
需要注意的是并不僅僅只有變長字段的列才會發生行溢出,blob,text都有可能,甚至大于或等于768字節的固定長度的列也會被編碼為可變長度的列,它可以被存儲在頁面外。例如,如果字符集的最大字節長度大于3,一個CHAR(255)列可以超過768字節,正如utf8mb4那樣。
Dynamic和Compressed?格式
Dynamic和Compressed?格式,現在使?的MySQL版本是5.7,它的默認?格式就是Dynamic,這倆?格式和Compact?格式挺像,只不過在處理?溢出數據時有點?分歧。
它們不會在記錄的真實數據處存儲字段真實數據的前768個字節,?是把所有的字節都存儲到其他頁?中,只在記錄的真實數據處存儲其他頁?的地

參考文檔
- <<MySQL是怎樣運行的:從根兒上理解MYSQL>>
- 官方文檔: https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html
- 官方文檔: https://dev.mysql.com/doc/internals/en/innodb-field-contents.html
原文