InnoDB的記錄按行存儲在數(shù)據(jù)頁中。記錄在數(shù)據(jù)頁種的排布在《InnoDB頁面結(jié)構(gòu)》中已述及,本文重點介紹InnoDB的記錄格式。
1 行格式總覽
InnoDB規(guī)劃了26種行格式,分別對應(yīng)26種動物,首字母由A至Z:Antelope, Barracuda, Cheetah, Dragon, Elk, Fox, Gazelle, Hornet, Impala, Jaguar, Kangaroo, Leopard, Moose, Nautilus, Ocelot, Porpoise, Quail, Rabbit, Shark, Tiger, Urchin, Viper, Whale, Xenops, Yak, Zebra。目前InnoDB支持的行格式只有Antelope, Barracuda。而Antelope又具體細(xì)分為Redundant和Compact,Barracuda也具體細(xì)分為Dynamic和Compressed。創(chuàng)建InnoDB表時,可以通過 ROW_FORMAT=XXX子句指定行格式,例如:
Create Table t (a int, b varchar(1000) not null, c char(100), d varchar(100)) CHARSET=utf8mb3 ROW_FORMAT = COMPACT;
Redundant是MySQL 5.0之前的行格式,它存儲的記錄是非緊湊類型的,比較占用磁盤空間。同樣的頁面中存儲的記錄行更少,索引的效率較低。目前已很少使用。Compact、Dynamic、Compressed三種行格式結(jié)構(gòu)比較相似。由于MySQL 5.7和8.0默認(rèn)的行格式為Dynamic,下面將展開介紹Dynamic行格式。
2 Dynamic格式
Dynamic行格式的級別結(jié)構(gòu)如下:
| 變長字段長度列表 | NULL值列表 | 記錄頭信息 | 系統(tǒng)列 | Field 1 | ... | Field N |
|---|
2.1 變長字段長度列表
對于Varchar、Text、Blob等這類變長的字段,其存儲長度是變長的。即使對于長度相同的字段,例如CHAR(10),雖然其存儲的字符是固定10個,用戶輸入的字符不足10個也將補齊至10個,但如果字符集是可以使用1-3個字節(jié)存儲字符的utf8mb3,其存儲字符的字節(jié)數(shù)也是變長的。InnoDB為了能準(zhǔn)確劃分、解析不同的字段,在每條記錄的第一步部分會記錄所有變長字段的長度。注意,例如Int固定長度和為空的變長字段的長度是不會記錄于此的。
具體而言,每個字段的長度使用1-2字節(jié)記錄。MySQL對字段由65535長度的限制也源自于此,因為2字節(jié)由16bit組成,能描述最大的數(shù)字為(2^16) - 1 = 65535。
每個字段的長度用1-2字節(jié)表示,那按什么規(guī)則區(qū)分是1個字節(jié)還是2個字節(jié)呢?在介紹規(guī)則之前先需要了解變長字段的最大可能長度的概念。變長字段的最大可能長度的計算方法為最大字符數(shù) * 字符集最大字節(jié)數(shù),例如上表中列b的最大字節(jié)數(shù)是b,字符集單字符最大字節(jié)數(shù)是3,那么最大可能長度為30。當(dāng)變長字段的最大可能長度大于255時,用一個字節(jié)記錄其長度。當(dāng)變長字段的最大可能長度大于255時,使用1-2字節(jié)描述字段長度。具體使用1個字節(jié)還是2個字節(jié),使用第一個字節(jié)的最高bit作為區(qū)分:如果其為0,表示只使用了一個字節(jié),如果為1表示使用了2個字節(jié)。當(dāng)只使用一個字節(jié)時,由于最高bit被用作標(biāo)志,所以其能表示的真實長度的范圍是[0, 127],當(dāng)真實長度大于127時,需要使用2個字節(jié)表示。
單個頁面大小只有16384字節(jié),而InnoDB規(guī)定單個頁面至少需要存放兩條記錄,那么一條記錄最大不得超過8192字節(jié)。實際上,算上索引中FIl Header、Page Header、Page Directory、Fil Trailer的空間,那么在頁面中存儲的記錄的長度更小。當(dāng)記錄超過限制大小時,會出現(xiàn)行溢出的現(xiàn)象,溢出頁的格式將在第三節(jié)討論。記錄溢出時,對應(yīng)變長字段的第一字節(jié)的第二個bit會對其進(jìn)行標(biāo)記,在變長字段長度列表處只存儲留在本頁面中的長度。至此,變長字段兩個字節(jié)中的16個bit已經(jīng)有兩個bit用作標(biāo)志(是否用兩字節(jié)存儲長度,是否有行外數(shù)據(jù)),還能用于描述字段長度的最大bit數(shù)為14,即最大能表示(2^14) - 1 = 16383字節(jié),描述存儲于當(dāng)前數(shù)據(jù)頁的記錄長度仍然綽綽有余。
除上述規(guī)則之外,還需要注意的是變長字段長度列表的存儲是按照字段的逆序存放的,與真實數(shù)據(jù)的存放的順序相反。例如上例中的表t的變長字段b, c, d在變長字段列表中的順序是d, c, b。
2.2 NULL值列表
為了節(jié)約空間,值為NULL的字段不會占用存儲空間,而是通過NULL標(biāo)記位記錄。只有可能為NULL之的字段才有可能出現(xiàn)在NULL值列表中,如果一個表的所有列都用NOT NULL修飾,則該表所有記錄都沒有NULL值列表。
NULL值列表通過BITMAP來標(biāo)識每個字段是否為空,每個可能為NULL的字段占一個bit位標(biāo)識,如果字段為空,則為1,否則為0。與變長字段列表相似,所有的NULL值也按照字段順序逆序排布。NULL列表占用的存儲空間一定是8 bit的整數(shù)倍,即按字節(jié)為單位存儲,如果可以為NULL的字段數(shù)不足8的倍數(shù),在NULL值列表的高位補0。
2.3 記錄頭信息
記錄頭的信息在《InnoDB頁面結(jié)構(gòu)》中已有部分介紹,此處對其所有內(nèi)容進(jìn)行介紹。記錄頭包含的信息如下:
| 內(nèi)容 | 大小 | 含義 |
|---|---|---|
| 預(yù)留位 | 1 | 暫未使用 |
| 預(yù)留位 | 1 | 暫未使用 |
| delete_flag | 1 | 是否刪除的標(biāo)識,如果刪除為1,為多版本并發(fā)控制服務(wù)(Multi-Version Concurrency Control ,MVCC) |
| min_rec_flag | 1 | B+樹非葉子結(jié)點中每一層最小的記錄會添加此標(biāo)識 |
| n_owned | 4 | 如果有Slot指向此記錄,此字段會有值并定表此為組長記錄,記錄此Slot管理的記錄數(shù) |
| heap_no | 13 | 記錄在頁面中的物理位置(堆上的位置),每申請一塊記錄空間,都會為其分配一個 heap_no,從前往后編號,標(biāo)記刪除的記錄不會減小heap_no |
| record_type | 3 | 記錄的類型,0表示葉子結(jié)點的用戶記錄,1表示非葉子結(jié)點的記錄,2表示Infimum記錄,3表示Supremum記錄 |
| next_record | 16 | 下一條記錄的地址,將頁面內(nèi)的記錄串聯(lián)起來 |
2.4 系統(tǒng)列
InnoDB聚簇索引可能會存在下述三個用戶不可見的隱藏系統(tǒng)列:
| 列名 | 是否必須 | 占用空間 | 描述 |
|---|---|---|---|
| DB_ROW_ID | 否 | 6字節(jié) | 行ID,唯一標(biāo)識一條記錄 |
| DB_TRX_ID | 是 | 6字節(jié) | 事務(wù)ID |
| DB_ROLL_PTR | 是 | 7字節(jié) | 回滾指針 |
- DB_ROW_ID:聚簇索引優(yōu)先使用用戶自定義的主鍵作為Key構(gòu)建B+樹,如果用戶沒有定義主鍵,則選取一個Unique鍵作為主鍵,如果表中連Unique鍵都沒有定義的話,則InnoDB會為表默認(rèn)添加此隱藏列作為主鍵。所以此列只有在無主鍵并且無Unique Key的表中存在。
此列只有在無主鍵表中才存在。由于用戶沒設(shè)置主鍵,InnoDB只能自己添加一個自增列作為key來構(gòu)建B+樹。 - DB_TRX_ID:表示該行最新修改的事務(wù)ID,為MVCC判斷記錄可見性服務(wù)
- DB_ROLL_PTR:回滾段指針,指向記錄的上一個版本,同樣為MVCC判斷記錄可見性服務(wù),當(dāng)前記錄經(jīng)MVCC判斷不可見時,通過該指針往前回溯記錄的舊版本,找到滿足可見性要求的記錄返回給用戶
二級索引記錄沒有DB_TRX_ID和DB_ROLL_PTR,所以其MVCC比較麻煩。二級索引頁的Page Header有MAX_TRX_ID字段,表示更新該頁面的最大事務(wù)ID。如果MAX_TRX_ID小于當(dāng)前事務(wù)開啟時的最小事務(wù)ID,那么萬事大吉,此二級索引頁面中的非標(biāo)記刪除的二級索引記錄都是可見的。否則,就需要從二級索引訪問到聚簇索引,通過聚簇索引再判斷記錄的可見性。
2.5 用戶列
用戶列與列之間沒有間隔,連續(xù)存放。
3 行溢出處理
3.1 行溢出時記錄的格式
當(dāng)變長的字段數(shù)據(jù)過長,導(dǎo)致索引頁無法容納兩條記錄,InnoDB會將過長的字段內(nèi)容存儲到外部存儲頁(blob page)。不同行格式在此處的處理略有不同。Antelope(Redundant和Compact)會Field內(nèi)容處存儲數(shù)據(jù)內(nèi)容的768字節(jié) + 行外數(shù)據(jù)等地址指針。而Barracuda(Dynamic和Compressed)只在Field內(nèi)容處記錄行外數(shù)據(jù)等地址指針。
行外數(shù)據(jù)等地址指針占20字節(jié),格式如下:
| 名稱 | 大小 | 內(nèi)容 |
|---|---|---|
| BTR_EXTERN_SPACE_ID | 4 | 外部存儲頁的space id |
| BTR_EXTERN_PAGE_NO | 4 | 外部存儲頁的頁碼 |
| BTR_EXTERN_OFFSET | 4 | 外部存儲頁的頁內(nèi)偏移。 |
| BTR_EXTERN_LEN | 8 | 數(shù)據(jù)的總大小 |
- BTR_EXTERN_OFFSET的取值分兩種情況:當(dāng)外部存儲頁不是壓縮頁時,該值為38。其指向外部存儲頁的Blob Header;當(dāng)外部存儲頁時壓縮頁時,該值為12,指向Fil Header部分的FIL_PAGE_NEXT。
- BTR_EXTERN_LEN盡管有8個字節(jié)可以存儲BLOB數(shù)據(jù)的總大小,但實際上只使用了最后4個字節(jié)。這意味著在InnoDB中,單個BLOB字段的最大大小目前為4GB。
3.2 非壓縮外部存儲頁結(jié)構(gòu)
在非壓縮頁格式中,外部存儲頁的管理結(jié)構(gòu)由FIl Header、Blob header、Blob data、Fil Trailer組成,溢出行中地址將指向Blob header。(關(guān)于Fil Header的介紹詳見《InnoDB頁面結(jié)構(gòu)》)。非壓縮外部存儲頁的結(jié)構(gòu)如下:

Blob header的組成如下:
| 內(nèi)容 | 大小 | 含義 |
|---|---|---|
| BTR_BLOB_HDR_PART_LEN | 4 | 當(dāng)前頁中存儲的字段的長度 |
| BTR_BLOB_HDR_NEXT_PAGE_NO | 4 | 如果當(dāng)前頁面未能存儲所有字段的全部數(shù)據(jù),會指向下一個外部存儲頁面的Page no。 |
3.3 壓縮外部存儲頁結(jié)構(gòu)
如果外部存儲頁為壓縮格式,其直接由Fil Header、壓縮數(shù)據(jù)、Fil Trailer組成。溢出行中地址將指向Fil Header中的FIL_PAGE_NEXT(頁內(nèi)偏移為12)。壓縮外部存儲頁的結(jié)構(gòu)如下圖所示:

4 其他行格式對比
4.2 Redundant
如前所述,Redundant是非緊湊型行格式,比較占用磁盤空間。Redundant行格式與Dynamic格式的不同之處在于并沒有區(qū)分定長和變長字段,而是將所有列占用的存儲空間都逆序存儲在字段長度偏移列表中。并且 Redundant格式并不存在NULL值列表,使用字段長度值的第1位來判斷字段是否為空,如果第1位為1,則為空。因為第1位用來記錄字段是否為NULL,所以一個字節(jié)所能表示的最大長度為127。
Redundant格式的記錄頭占用了6個字節(jié),分為了9部分,相較于Dynamic格式多了n_field和1byte_offs_flag字段,少了record_type字段,格式如下所示:
| 名稱 | 大小 | 內(nèi)容 |
|---|---|---|
| 預(yù)留位 | 1 | 暫未使用 |
| 預(yù)留位 | 1 | 暫未使用 |
| delete_flag | 1 | 是否刪除的標(biāo)識,如果刪除為1 |
| min_rec_flag | 1 | B+樹非葉子結(jié)點中每一層最小的記錄會添加此標(biāo)識 |
| n_owned | 4 | 如果有slot指向此記錄,此字段會有值,記錄此slot管理的記錄數(shù) |
| heap_no | 13 | 記錄在頁面中的物理位置(堆上的位置),每申請一塊記錄空間,都會為其分配一個 heap_no,從前往后編號 |
| n_field | 10 | 記錄中列的數(shù)量 |
| 1byte_offs_flag | 1 | 標(biāo)識字段長度偏移列表中字段的長度用1個字節(jié)還是2個字節(jié)來表示,如果所有字段長度小于127,則用一個字節(jié)表示,如果大于127,則用兩個字段表示 |
| next_record | 16 | 下一條記錄的地址,將頁面內(nèi)的記錄串聯(lián)起來 |
4.2 Compact
Compact是一種緊湊類型的存儲格式,與Dynamic類型的存儲格式基本一致。如第三節(jié)所述,作為Antelope,其溢出行的處理方式是在索引頁存儲變長字段的前768字節(jié)的數(shù)據(jù)+外部存儲頁指針,因此其變長字段長度為768+20。與Redundant格式相比,Compact行格式減少了約20%的行存儲空間。
4.3 Compressed
Compressed類型與Dynamic類型擁有相同的存儲特性和功能,不同之處在于使用壓縮算法對頁面進(jìn)行壓縮,包括溢出頁。優(yōu)點在于可以節(jié)約存儲空間,但是在查找數(shù)據(jù)時需要先解壓才行,會消耗更多的CPU資源。
Compressed行格式必須在建表時指定,而且需要同時指定KEY_BLOCK_SIZE。KEY_BLOCK_SIZE會控制壓縮后頁面的大小,指定的大小必須小于當(dāng)前默認(rèn)數(shù)據(jù)頁的大小。如果沒有指定KEY_BLOCK_SIZE,則會自動設(shè)置為默認(rèn)數(shù)據(jù)頁大小的一半。如果要使通用表空間包含壓縮表,必須指定FILE_BLOCK_SIZE選項,如果小于當(dāng)前默認(rèn)數(shù)據(jù)頁的大小,會自動設(shè)置為Compressed格式。其中FILE_BLOCK_SIZE的單位為Byte,KEY_BLOCK_SIZE的單位為KB。