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