|InnoDB資料頁詳解

語言: CN / TW / HK

提示:公眾號展示程式碼會自動折行,建議橫屏閱讀

「前言」

InnoDB層的檔案除日誌檔案外,都具有較為統一的物理結構。所有物理檔案由頁(page 或 block)構成,在未被壓縮情況下,一個頁的大小為UNIV_PAGE_SIZE(16384,16K)。不同用途的頁具有相同格式的檔案頭和檔案尾,其中記錄了頁面校驗值、頁面編號、表空間編號、LSN等通用資訊。根據不同的應用場景和功能可以將頁面分為多種型別,比如:每隔一定數量的頁面後會使用extern描述頁來記錄每頁空閒與否;Inode頁面用於儲存segment資訊,segment是表空間管理的邏輯單位,每個索引佔用2個segment,分別用於管理葉子節點和非葉子節點;索引頁用於儲存索引和使用者記錄;Blob頁面用於記錄溢位行的內容等等。InnoDB檔案的結構可以詳見《淺析InnoDB檔案結構》。

本文主要討論使用者記錄儲存相關的資料頁面(索引頁和外部儲存頁)的物理結構以及組織方式。InnoDB用B+樹的方式管理使用者記錄資料,每個索引對應一個B+樹。B+樹是通過索引頁構建的,使用者記錄的資料儲存在聚簇索引的葉子結點中。如果有變長欄位(如text、blob、varchar)的長度過長,則可能會將該欄位的全部資料或部分資料儲存到外部儲存頁(blob頁面)。

本文在第一部分、第二部分中詳細介紹了索引頁和外部儲存頁的組織方式,並以實際的資料檔案進行舉例說明。注意,前兩部分以MySQL5.7為基礎,介紹了非壓縮頁、dynamic型別的行格式的情況。在第三部分對壓縮頁和其他型別的行格式進行了補充說明。

「第一部分 索引頁」

每個B+樹通過兩個segment來管理資料頁,一個管理非葉子結點,一個管理葉子結點。這兩個segment儲存在B+樹的root page中,在獨立表空間中,聚簇索引的root page通常為第3個頁面(從0開始計數)。索引頁由七部分組成:檔案頭(Fil Header)、頁頭(Page Header)、最大最小記錄(Infimum and Supremum Records)、使用者記錄(User Records)、空餘空間(Free Space)、資料目錄(Page Directory)、檔案尾(Fil Trailer),其組成如下圖所示:

為便於理解,本文引入了具體的例子來進行分析介紹,其建表語句以及資料操作的資訊如下:

CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c1` varchar(20) DEFAULT NULL,
`c2` varchar(60000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;


INSERT INTO t(id, c1, c2) VALUES(1, 'a', '111aaa');
INSERT INTO t(id, c1, c2) VALUES(2, 'b', '222bbb');
INSERT INTO t(id, c1, c2) VALUES(3, 'c', '333ccc');


DELETE FROM t WHERE id = 2;

接下來將從上述七個部分對索引頁進行詳細的介紹。

1.1 檔案頭(Fil Header)

任何型別的頁面都擁有38個位元組的檔案頭,其主要用於checksum校驗、頁面型別判斷、獲取前後頁面等,由以下8個部分組成:

補充說明:

1.FIL_PAGE_SPACE_OR_CHKSUM主要用來儲存checksum,這裡的checksum與檔案尾的相對應,對於checksum的計算以及驗證方式在檔案尾部分進行詳細的解釋。

2.FIL_PAGE_LSN記錄了最近修改的一次LSN。在崩潰恢復時,如果redo log的LSN小於當前的LSN,redo日誌就不需要應用了。除此之外,該欄位也應用於簡單地判斷頁面是否損壞,頁面最後4個位元組的值與該欄位後四位的值是一樣的,並且如果該LSN大於系統目前最大的LSN號,也會認為頁面是損壞的。

接下來通過hexdump檢視上述例子中索引頁的Fil Header。對於獨立表空間而言,其索引頁從第3頁開始,且該頁為B+樹的root page。頁面大小為16384,因此root page的Fil Header的偏移為 3 * 16384 = 49152 = 0xc000 。下面是該頁面的Fil Header的實際內容:

$hexdump -s 0xc000 -n 38 -C t.ibd 
0000c000 63 ad 74 69 00 00 00 03 ff ff ff ff ff ff ff ff |c.ti............|
0000c010 00 00 00 00 a8 cf e0 a4 45 bf 00 00 00 00 00 00 |........E.......|
0000c020 00 00 00 00 00 55 |.....U|
0000c026

其中 00 00 00 03 為頁碼,頁碼為3。因為在此例子中只有3條記錄,B+樹只使用了一個頁面進行儲存,所以其前一個和下一個頁面均為 ff ff ff ff ,即FIL_NULL。頁面型別的值為 45 bf ,為FIL_PAGE_INDEX,表示當前頁面是索引頁。

1.2 頁頭(Page Header)

這部分儲存的是索引頁的元資訊,這部分由56個位元組14個部分組成,其組成如下:

補充說明:

1.PAGE_N_DIR_SLOTS至少有兩個,一個指向最小記錄,一個指向最大記錄,每個slot管理的記錄為4~8個記錄。關於page directory和slot的詳細資訊請看資料目錄部分的介紹。

2.PAGE_LAST_INSERT、PAGE_DIRECTION、PAGE_N_DIRECTION主要用於加速插入操作。

3.PAGE_N_HEAP在建立空頁的時候預設為2,即包含了最大記錄、最小記錄這兩條系統記錄。這個欄位同時也用於判斷記錄的格式,如果第一個bit為0,則表示記錄格式redundant型別。如果第一個bit為1,則記錄格式為緊湊型的行格式(compact、dynamic或compressed格式,這三種方式的儲存特性是一致的,唯一的區別在於行溢位時的處理,但是對未壓縮的資料的解析流程是一致的)。

4.PAGE_BTR_SEG_LEAF和PAGE_BTR_SEG_TOP只在B+樹的root page被設定,其他頁面這兩個欄位的內容是未被使用的。這兩個欄位通過10個位元組描述了對應的segment在inode page中的位置,其記錄的內容如下:

接下來檢視例子中root page的頁頭的實際儲存內容,其在檔案內的偏移0xc026為:root page的偏移(0xc000)+ Fil Header的長度38(0x26),內容如下:

$hexdump -s 0xc026 -n 56 -C t.ibd   
0000c026 00 02 00 d8 80 05 00 a0 00 20 00 00 00 02 00 02 |......... ......|
0000c036 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
0000c046 00 00 00 68 00 00 00 55 00 00 00 02 00 f2 00 00 |...h...U........|
0000c056 00 55 00 00 00 02 00 32 |.U.....2|
0000c05e

部分內容解析說明:

1.0xc026~0xc028 -->00 02 :PAGE_N_DIR_SLOTS。從ibd的內容可以解析得到page directory中slot的數量為2,分別指向最大和最小記錄。因為一個slot至少擁有4條記錄,本例子中只有2條使用者記錄,所有隻有兩個預設的slot。

2.0xc028~0xc02a -->00 d8 :PAGE_N_HEAP。空餘空間(Free Space)起始地址的頁面偏移為 00 d8 ,由此可以計算剩餘空間在檔案內的偏移為 0xc000 + 0xd8 = 0xc0d8 ,檢視0xc0d8地址的開始的內容全部為0。

$hexdump -s 0xc0d8 -n 100 -C t.ibd  
0000c0d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
0000c138 00 00 00 00 |....|
0000c13c

3.0xc02a~0xc02c -->80 05 :PAGE_N_HEAP。第一個bit為1,表示記錄為dynamic格式。剩餘的bit計算的記錄數為5。其由兩條系統記錄(Infimum和Supremum記錄)、兩條使用者記錄(id='1'和id='3')、以及一條刪除的記錄(id='2')組成。

4.0xc02c~0xc02e -->00 a0 :PAGE_FREE。第一條被刪除的記錄的相較於頁面的偏移為 00 a0 ,檢視其對應的記錄如下。可以觀察到其的確指向了被刪除的記錄,其中 80 00 00 02 為主鍵id的值,關於記錄的組織方式會在後面進行詳細的解釋。

$hexdump -s 0xc0a0 -n 24 -C t.ibd  
0000c0a0 80 00 00 02 00 00 00 00 61 3c 38 00 00 02 a0 05 |........a<8.....|
0000c0b0 31 62 32 32 32 62 62 62 |1b222bbb|
0000c0b8

5.0xc036~0xc038 -->00 02:PAGE_N_RECS。值為 2 ,與id='1'和id='3'兩條記錄正確對應。

6.0xc040~0xc042 -->00 00:PAGE_LEVEL。值為 0 ,表示此頁面為葉子頁面,儲存了使用者的記錄。

1.3 記錄儲存格式及解析

注意:本節例子中行的格式為dynamic型別,因此本節的內容主要基於dynamic型別的行格式。其他行格式補充在第三部分。Dynamic型別的行格式如下所示:

1.3.1 變長長度列表

對於varchar、text、blob等這類變長的欄位,其儲存長度是變長的。如果不記錄該欄位佔用的真實長度,那麼則無法正確地切分不同的欄位,也無法正確的對欄位進行解析。因此對於變長型別的欄位不僅需要儲存其真實的資料,還需要記錄其真實長度。變長欄位的長度便儲存在變長欄位長度列表中。

變長欄位的儲存以及解析遵循以下規則:

1.變長欄位長度列表的儲存是按照欄位的逆序存放的,與真實資料的存放的順序相反。

2.變長欄位為空時,不會存在於變長欄位長度列表中。

3.變長欄位的最大長度小於255時,用一個位元組記錄其長度;最大長度大於255時,如果真實長度小於127,用一個位元組表示,如果真實長度大於127時,用兩個位元組表示。比如對於一個VARCHAR(10000)的欄位,如果其真實的長度為255位元組,則會用兩個位元組記錄其長度,如果真實長度為100位元組,會使用一個位元組儲存該欄位的長度。

4.對於最大長度大於255的記錄,當在變長欄位列表中對應的長度值的第一個位元組的第一個位元位為0時,表示該長度用一個位元組表示,為1則表示用兩個位元組表示。第二個位元位為1時,則表示該欄位有行外資料儲存在blob型別的頁面上。對於行外儲存的情況將在第二部分進行介紹。

注意:上述的長度為位元組的長度而並非字元長度。比如在UTF-8編碼中,一箇中文字元佔用的位元組為3,如果一個變長欄位儲存了2箇中文字元,其記錄的長度為位元組長度6,而並非2。

1.3.2 NULL值列表

為了節約空間,NULL值並不會佔用儲存空間。因此通過NULL值列表記錄哪些欄位為NULL,告知哪些欄位不需要進行解析。

其主要遵循以下規則:

1.只有可以為NULL的欄位才會在NULL值列表中。

2.NULL值列表通過BITMAP來標識每個欄位是否為空,一個欄位用一個位元位標識。如果欄位為空,則為1,否則為0。

3.NULL值列表是按照欄位順序逆序排列的。

4.NULL列表佔用的儲存空間一定是8 bit的整數倍,如果可以為NULL的欄位數不足8的倍數,在NULL值列表的高位補0。

1.3.3 記錄頭資訊

記錄頭資訊記錄了記錄的型別、下一條記錄的地址等資訊,固定頭部資訊佔用5個位元組,其組成如下:

1.3.4 最大最小記錄解析示例

Infimum和Supremum記錄為系統記錄,沒有變長欄位列表和NULL值列表,只包含記錄頭資訊和真實資料。其next_record分別指向頁面中最小和最大的記錄。它們的真實資料部分儲存了字串"infimum\0"和"supremum",其中在Dynamic格式中,infimum字串後有一個0值,supremum記錄沒有。本節主要通過Infimum和Supremum記錄的來了解記錄頭資訊的解析。

$hexdump -s 0xc05e -n 26 -C t.ibd
0000c05e 01 00 02 00 1d 69 6e 66 69 6d 75 6d 00 03 00 0b |.....infimum....|
0000c06e 00 00 73 75 70 72 65 6d 75 6d |..supremum|
0000c078

部分內容解析說明:

1. n_owned欄位解析 :n_owned與前面四個部分共同佔用一個位元組,位元組的後四位用來記錄n_owned的值。其中Infimum的n_owned欄位值為1,其slot只管理Infimum自身。Supremum記錄的n_owned欄位的值為3,其管理supremum記錄、以及兩條使用者記錄(id='3'和id='1')。

2. record_type欄位解析 :heap_no和record_type共佔2個位元組。以supremum為例,heap_no和record_type佔用兩個位元組的值為 00b0 ,其每個位元的值為 0000000000001 011 ,record_type用後3個位元表示,record_type的值為3。同樣可以計算得到Infimum記錄的record_type為2。

3. next_record地段解析 :Infimum的next_record指向的是第一條記錄的真實資料的位置,next_record地址是相對於當前記錄的真實資料地址的偏移,因此第一條記錄的真實資料的地址為:0xc05e + 0x05 (記錄頭大小) + 0x001d (next_record的值) = 0xc080。

1.3.5 使用者記錄解析示例

從1.3.4的中解析得到第一條記錄的真實資料的地址為0xc080,因為真實資料前有5個位元組的記錄頭資訊、1個位元組的NULL值列表以及2個位元組的變長欄位列表,因此我們從地址0xc080-0x08=0xc078開始檢視第一條記錄的資訊:

$hexdump -s 0xc078 -n 26 -C t.ibd
0000c078 06 01 00 00 00 10 00 40 80 00 00 01 00 00 00 00 |[email protected]|
0000c088 61 39 b5 00 00 01 2a 01 10 61 31 31 31 61 61 61 |a9....*..a111aaa|
0000c098

部分內容解析說明

1. 變長欄位長度列表 :變長欄位長度列表的值為 06 01 ,其按照欄位的順序逆序儲存。欄位c1的最大字元長度為20,其最大的位元組長度小於255,因此其欄位長度用一個位元組 01 表示,其真實資料佔用的位元組數為1。欄位c2的最大位元組長度大於255,因此既有可能用一個位元組記錄長度也可能用兩個位元組記錄其長度,而 06 的第一個位元位0,表示其用一個位元組表示其長度,c2真實資料佔用6個位元組。

2. NULL值列表 :該表只有兩個欄位c1和c2可以取空值,因此使用一個位元組來記錄空值列表。空值列表的值為 00 ,表示真實資料中沒有欄位取NULL。
3. 記錄頭資訊 :接下來是記錄頭資訊 00 00 10 00 40 ,記錄頭的資訊在Infimum和Supremum記錄頭部分的解析解釋過了,在此部分不再過多解釋了。
4. 真實資料部分解析 :真實資料部分的第一部分為主鍵id。前四個位元組 80 00 00 01 是主鍵的id,值為1。如果在沒有指定主鍵的情況下,預設使用前6個位元組記錄MySQL自己生成的主鍵row_id。真實資料的第二部分為系統生成的隱藏列,使用者不可見。主鍵後面6個位元組 00 00 00 00 61 39 為事務ID,接下來7個位元組為 b5 00 00 01 2a 01 10 為回滾指標。回滾段指標用於指向修改前的資料(資料的上一個版本),事務ID和回滾指標用於多版本併發控制(mvcc),當undo中的記錄不再被使用者需要時,會由後臺的purge執行緒回收。隱藏列後的內容為使用者自定義的欄位,由變長欄位列表可知c1的長度為1,故系統欄位後的第一個位元組 61 為c1的值,值為'a'。c2的長度為6, 31 31 31 61 61 61 為c2儲存的內容,值為 111aaa 。

其解析的內容總結如下:

06 01                     變長欄位列表,c1長度1,c2長度為2
00 NULL值列表
00 00 10 00 40 固定記錄頭資訊
80 00 00 01 主鍵,欄位id的值,1
00 00 00 00 61 39 事務ID
b5 00 00 01 2a 01 10 回滾指標
61 欄位c1的值'a'
31 31 31 61 61 61 欄位c2的值'111aaa'

1.4 空餘空間(Free Space)

這部分是介於使用者記錄和Page directory之間的一塊連續的未被使用的記憶體。在空間足夠時,會直接從這裡分配記憶體,當空間不足時,會重新整理頁面內的記錄,將碎片空間進行合併。在將空間分配給記錄後,會遞增PAGE_N_RECS和PAGE_N_HEAP的值。

1.5 資料目錄(Page Directory)

資料目錄可以理解為頁內的索引。其由一個個slot組成,每個slot由兩個位元組組成,每個slot指向一條記錄,slot的值是記錄在頁面內的偏移。每個slot管理4~8條記錄。在查詢時首先資料目錄上對slot進行二分查詢,定位到具體的slot後,然後在slot內進行順序查詢。頁內slot的組織方式如下:

注意:slot的順序應當是逆序儲存的,圖中為了避免箭頭交叉才順序畫的。

slot分配是從頁面的最後倒數8個位元組開始逆序分配的,一個page至少有兩個slot,第一個slot指向Infimum記錄,最後一個slot指向Supremum記錄。

接下來看一下例子中資料目錄的具體儲存內容。在page header部分解析得到page directory中slot的個數為2。那麼page directory的偏移為:0xfff4 = 0x10000(頁面結束地址)- 0x08(頁面末尾checksum和LSN後四位)- 0x04(一共兩個slot四個位元組)。資料目錄的儲存的內容如下:

$hexdump -s 0xfff4 -n 4 -C t.ibd
0000fff4 00 70 00 63 |.p.c|
0000fff8

因為頁面的資料比較少,只有兩個slot,分別指向Infimum和Supremum記錄。下面進行簡單的驗證:

$hexdump -s 0xc063 -n 8 -C t.ibd
0000c063 69 6e 66 69 6d 75 6d 00 |infimum.|
0000c06b


$hexdump -s 0xc070 -n 8 -C t.ibd
0000c070 73 75 70 72 65 6d 75 6d |supremum|
0000c078

其確實指向了Infimum和Supremum記錄的真實資料部分。

1.6 檔案尾(Fil Trailer)

檔案尾的作用是校驗檔案是否損壞,在每個頁面的結尾的8個位元組中,分別儲存了checksum和LSN的後四位,對應於FIL HEADER中的內容。下面是root page中檔案尾的儲存內容:

$hexdump -s 0xfff8 -n 8 -C t.ibd
0000fff8 63 ad 74 69 a8 cf e0 a4 |c.ti....|
00010000

可以看到checksum的值為 63 ad 74 69 ,LSN後四位的值為 a8 cf e0 a4 ,與檔案頭內的內容是一致的。
MySQL中提供瞭如下6種頁面checksum值計算的模式:

/** Possible values of the parameter innodb_checksum_algorithm */
static const char* innodb_checksum_algorithm_names[] = {
"crc32",
"strict_crc32",
"innodb",
"strict_innodb",
"none",
"strict_none",
NullS
};

上述六種校驗模式實際上是三種演算法。其中crc32和innodb都會計算頁面的checksum,而對於none模式,這種方式並不會計算資料頁的校驗值,而是使用一個指定的值(BUF_NO_CHECKSUM_MAGIC)填充checksum欄位。crc32和none模式下頁面的檔案頭和檔案尾的checksum是相同的,在innodb校驗方式下,前後的checksum是不同的。這是因為在寫checksum之前,檔案尾的8個位元組會先寫8個位元組的LSN號。在crc32和none模式中後面會覆蓋檔案尾的checksum欄位。而在4.1版本之前,innodb校驗方法並不會修改檔案尾為checksum的sum欄位,而新的版本中,innodb方法會將LSN號的前四個位元組計算校驗和,並覆蓋檔案尾的checksum欄位。

在指定某種校驗演算法時,如果讀取的頁面時該演算法校驗失敗,會繼續嘗試其他的演算法進行校驗。如果沒有指定strict開頭的演算法,指定方式校驗失敗會嘗試其餘的2種校驗方式,有一種通過校驗方法通過就可以讀取頁面。如果指定了strict開頭的演算法,頁面必須通過指定校驗方法才可以讀取。

「第二部分 外部儲存頁」

2.1 行溢位時索引頁面記錄的格式

每個頁面至少應當儲存兩個記錄,如果一個頁面的大小為16k,那麼一條記錄最大不得超過8k,事實上應當更小,因為還有檔案頭、頁頭、檔案尾等部分需要儲存。如果變長的資料過長,導致索引頁無法容納兩條記錄,會將長度過長的欄位的內容儲存到外部儲存頁(blob page)。

在Dynamic格式中,過長欄位的內容會全部儲存到blob頁面,索引頁只儲存20位元組的指標指向外部儲存頁。其指標的內容如下所示:

在第一部分例子的基礎上,執行以下SQL語句插入長欄位。

insert into t(id, c1, c2) values(4,'4dd', REPEAT('d', 50000));

使用hexdump檢視新插入記錄的儲存內容:

$hexdump -s 0xc0d8 -n 49 -C t.ibd
0000c0d8 14 c0 03 00 00 00 28 00 31 80 00 00 04 00 00 00 |......(.1.......|
0000c0e8 00 61 3e b9 00 00 01 2e 01 10 34 64 64 00 00 00 |.a>.......4dd...|
0000c0f8 55 00 00 00 04 00 00 00 26 00 00 00 00 00 00 c3 |U.......&.......|
0000c108 50 |P|
0000c109

部分內容解析

1. c2欄位長度解析 :可以觀察到,c2在變長欄位長度列表中的值為c0,其對應的位元位為:11000000,第一個位元位為1,表示真實長度大於127,需要用兩個欄位表示欄位的長度,第二個位元位為1,表示c2欄位的值儲存在外部blob欄位中。剩餘的位元位與第二個位元組 14 記錄了c2欄位的長度,0x14的值為20,即只儲存了20位元組的指向外部儲存頁的指標。

2. 解析指向外部儲存頁的指標 :指標的內容為該記錄的最後20個位元組: 00 00 00 55 00 00 00 04 00 00 00 26 00 00 00 00 00 00 c3 50 。解析得到space id為0x55,外部儲存頁位於第4頁,在blob 頁面的偏移為0x26=38,表示外部儲存頁為非壓縮頁,總長度為0xc350=50000。

2.2 外部儲存頁結構

在非壓縮頁格式中,外部儲存頁由檔案頭、blob header、blob data組成。Blob header的組成如下:

非壓縮的情況下,外部儲存指標指向的頁面偏移為38,指向blob header起始地址,如下所示:

如果外部儲存頁為壓縮格式,其直接由檔案頭和壓縮資料組成,外部儲存頁指標指向的頁面偏移為12,指向FIL_PAGE_NEXT,如下所示:

接下來可以檢視2.1中的例子中外部儲存頁的內容,在章節2.1中解析得到blob頁面的頁碼為4,檢視頁碼為四的全部內容:

$hexdump -s 0x10000 -n 16384 -C t.ibd
00010000 23 1f 5c a2 00 00 00 04 00 00 00 00 00 00 00 00 |#.\.............|
00010010 00 00 00 00 a8 d0 65 a6 00 0a 00 00 00 00 00 00 |......e.........|
00010020 00 00 00 00 00 55 00 00 3f ca 00 00 00 05 64 64 |.....U..?.....dd|
00010030 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 |dddddddddddddddd|
*
00013ff0 64 64 64 64 64 64 64 64 23 1f 5c a2 a8 d0 65 a6 |dddddddd#.\...e.|
00014000

可以看到,除了檔案頭和檔案尾以及blob header外,剩下的全部內容均為欄位值,解析blob header的內容如下:

1.該頁中儲存的長度為 00 00 3f ca ,轉為10進製為16330,等於16384(頁面大小)- 38(檔案頭大小)- 8(檔案尾大小) - 8 (blob header)。

2.下一個blob的頁碼為 00 00 00 05 ,頁碼為5。

「第三部分 補充內容」

3.1 其他型別行格式

InnoDB儲存引擎支援四行格式:redundant、compact、dynamic和compressed。每種型別行格式的對比如下:

在章節1.3中詳細的介紹了dynamic格式,也是目前5.7和8.0預設的行格式。除redundant格式,compact、compressed格式與dynamic格式差異很小,接下來對其他三種行格式進行概述。

Redundant行格式:

Redundant格式是為了與MySQL5.0之前的版本相容,InnoDB檔案格式(Antelope和Barracuda)都支援Redundant格式。通常並不會採用redundant格式,因為它是非緊湊型別的,比較佔用磁碟空間,同樣的頁面中儲存的記錄行更少,索引的效率較低。其記錄的格式如下:

Redundant行格式與dynamic格式的不同之處在於並沒有區分定長和變長欄位,而是將所有列佔用的儲存空間都逆序儲存在欄位長度偏移列表中。並且redundant格式並不存在null值列表,使用欄位長度值的第1位來判斷欄位是否為空,如果第1位為1,則為空。因為第1位用來記錄欄位是否為NULL,所以一個位元組所能表示的最大長度為127。

Redundant格式的記錄頭佔用了6個位元組,分為了9部分,相較於dynamic格式多了n_field和1byte_offs_flag欄位,少了record_type欄位,格式如下所示:

除上述不同之處外,redundant格式對於行溢位的處理也區別於dynamic格式,其會在索引頁儲存變長欄位的前768位元組的資料+外部儲存頁指標。

Compact型別行格式

Compact是一種緊湊型別的儲存格式,與dynamic型別的儲存格式基本一致,區別之處主要在於溢位行的處理方式。Compact型別在處理行溢位時,與redundant型別一樣會在索引頁儲存變長欄位的前768位元組的資料+外部儲存頁指標。與redundant格式相比,compact行格式減少了約20%的行儲存空間。

Compressed型別行格式

Compressed型別與dynamic型別擁有相同的儲存特性和功能,不同之處在於使用壓縮演算法對頁面進行壓縮,包括溢位頁。優點在於可以節約儲存空間,但是在查詢資料時需要先解壓才行,會消耗更多的cpu資源。在建表時指定compressed格式,可以同時指定KEY_BLOCK_SIZE。KEY_BLOCK_SIZE會控制壓縮後頁面的大小,指定的大小必須小於當前預設資料頁的大小。如果沒有指定KEY_BLOCK_SIZE,則會自動設定為預設資料頁大小的一半。要使通用表空間包含壓縮表(ROW_FORMAT=COMPRESSED),必須指定FILE_BLOCK_SIZE選項,如果小於當前預設資料頁的大小,會自動設定為compressed格式。其中FILE_BLOCK_SIZE的單位為byte,KEY_BLOCK_SIZE的單位為kb。
Compressed格式實際上是進行表壓縮,具體可以參考3.2壓縮表部分。

3.2 壓縮頁

目前索引頁的壓縮形式存在兩種:一種是透明頁壓縮,一種是傳統的壓縮表格式。

透明頁壓縮

透明頁壓縮利用了Linux punch hole的新特性,對除檔案頭之外的地方進行壓縮,在寫入檔案後進行打洞操作,讀入檔案後進行解壓操作。其利用檔案頭部的FIL_PAGE_FILE_FLUSH_LSN欄位的8個位元組儲存壓縮資訊,內容如下:

在Linux系統上,檔案系統塊大小是用於打孔的單元大小。因此,只有當頁面資料可以壓縮到小於或等於InnoDB頁面大小減去檔案系統塊大小時,頁面壓縮才有效。例如,如果innodb_page_size=16K,並且檔案系統塊大小為4K,則頁面資料必須壓縮到小於或等於12K,才能進行打孔。透明頁壓縮的使用方式:

CREATE TABLE t1 (c1 INT) COMPRESSION="zlib";

壓縮表

壓縮表格式在進行更新或者插入操作時,資料會被儲存在頁面的mlog區域,當mlog快被填滿時,頁面才會重新進行壓縮。因此頁面中可能同時存在壓縮資料和非壓縮的資料。其頁面的具體格式如下:

啟用方式:

CREATE TABLE t1(c1 INT) ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=8;

「第四部分 總結」

本文詳細闡述了InnoDB用於儲存使用者記錄的相關頁面的物理儲存結構,並舉例對每一部分的內容進行了手動解析。本文主要以非壓縮頁、dynamic型別的行資料格式進行了舉例剖析,壓縮頁以及其他行格式的情況差異並不大,讀者可以根據文章第三部分補充的內容嘗試壓縮頁或其他型別行格式的解析。

「第五部分 參考文獻」

https://dev.mysql.com/doc/internals/en/innodb-record-structure.html

https://dev.mysql.com/doc/internals/en/innodb-page-structure.html

https://dev.mysql.com/doc/refman/5.7/en/innodb-compression-background.html

https://dev.mysql.com/blog-archive/externally-stored-fields-in-innodb/

https://dev.mysql.com/blog-archive/innodb-transparent-page-compression/

https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html

https://dev.mysql.com/doc/refman/5.7/en/general-tablespaces.html

騰訊資料庫技術團隊對內支援QQ空間、微信紅包、騰訊廣告、騰訊音樂、騰訊新聞等公司自研業務,對外在騰訊雲上依託於histore的底座,支援TencentDB相關產品,如TDSQL-C(原CynosDB)、TencentDB for MySQL(CDB)等。騰訊資料庫技術團隊專注於持續優化資料庫核心和架構能力,提升資料庫效能和穩定性,為騰訊自研業務和騰訊雲客戶提供“省心、放心”的資料庫服務。此公眾號旨在和廣大資料庫技術愛好者一起推廣和分享資料庫領域專業知識,希望對大家有所幫助。