InnoDB存儲引擎和大多數資料庫一樣(如Oracle和Microsoft SQL Server資料庫),記錄是以行的形式存儲的。這意味着頁中儲存着表中一行行的資料。到MySQL 5.1時,InnoDB存儲引擎提供了Compact和Redundant兩種格式來存放行記錄資料,Redundant是為相容之前版本而保留的,如果你閱讀過InnoDB的源代碼,會發現源代碼中是用PHYSICAL RECORD(NEW STYLE)和PHYSICAL RECORD(OLD STYLE)來區分兩種格式的。MySQL 5.1預設儲存為Compact行格式。你可以通過指令SHOW TABLE STATUS LIKE 'table_name'來檢視目前表使用的行格式,其中row_format就代表了目前使用的行記錄結構類型。例如:
use mysql;
show table status like 'user'\G

資料庫執行個體的一個作用就是讀取頁中存放的行記錄。如果我們知道規則,那麼也可以讀取其中的記錄,如之前的py_innodb_page_info工具。下面将具體分析各格式存放資料的規則。
Compact行記錄是在MySQL 5.0時被引入的,其設計目标是能高效存放資料。簡單來說,如果一個頁中存放的行資料越多,其性能就越高。Compact行記錄以如下方式進行存儲:
Compact行格式的首部是一個非NULL變長字段長度清單,而且是按照列的順序逆序放置的。當列的長度小于255位元組,用1位元組表示,若大于255個位元組,用2個位元組表示,變長字段的長度最大不可以超過2個位元組(這也很好地解釋了為什麼MySQL中varchar的最大長度為65 535,因為2個位元組為16位,即216=1=65 535)。第二個部分是NULL标志位,該位訓示了該行資料中是否有NULL值,用1表示。該部分所占的位元組應該為bytes。接下去的部分是為記錄頭資訊(record header),固定占用5個位元組(40位),每位的含義見下表4-1。最後的部分就是實際存儲的每個列的資料了,需要特别注意的是,NULL不占該部分任何資料,即NULL除了占有NULL标志位,實際存儲不占有任何空間。另外有一點需要注意的是,每行資料除了使用者定義的列外,還有兩個隐藏列,事務ID列和復原指針列,分别為6個位元組和7個位元組的大小。若InnoDB表沒有定義Primary Key,每行還會增加一個6位元組的RowID列。
下面用一個具體事例來分析Compact行記錄的内部結構:
create table mytest (
t1 varchar(10),
t2 varchar(10),
t3 char(10),
t4 varchar(10)
) engine=innodb charset=latin1 row_format=compact;
insert into mytest values('a','bb','bb','ccc');
insert into mytest values('d','ee','ee','fff');
insert into mytest values('d',NULL,NULL,'fff');
select * from mytest\G;
建立了mytest表,有4個列,t1、t2、t4都為varchar變長字段類型,t3為固定長度類型char。接着我們插入了3條有代表性的資料,接着打開mytest.ibd(啟用了innodb_file_per_table,若你沒有啟用該選項,請打開預設的共享表空間檔案ibdata1)。在Windows下,可以選擇用UltraEdit打開該二進制檔案(在Linux環境下,使用hexdump -C -v mytest.ibd>mytest.txt即可),打開mytest.txt檔案,找到如下内容:
現在第一行資料就展現在我們眼前了。需要注意的是,變長字段長度清單是逆序存放的,03 02 01,而不是01 02 03。還需要注意的是InnoDB每行有隐藏列。同時可以看到,固定長度char字段在未填充滿其長度時,會用0x20來進行填充。再來分析一下,記錄頭資訊的最後4個位元組代表next_recorder,0x6800代表下一個記錄的偏移量,目前記錄的位置+0x6800就是下一條記錄的起始位置。是以InnoDB存儲引擎在頁内部是通過一種連結清單的結構來串聯各個行記錄的。
第二行我将不做整理,除了RowID不同外,它和第一行大同小異,有興趣的讀者可以用上面的方法自己試試。
現在我們關注有NULL值的第三行:
03 01/*變長字段長度清單,逆序*/
06/*NULL标志位,第三行有NULL值*/
00 00 20 ff 98/*記錄頭資訊*/
00 00 00 2b 68 02/*RowID*/
00 00 00 00 06 07/*TransactionID*/
80 00 00 00 32 01 10/*Roll Pointer*/
64/*列1資料'd'*/
66 66 66/*列4資料'fff'*/
第三行有NULL值,是以NULL标志位不再是00而是06了,轉換成二進制為00000110,為1的值即代表了第2列和第3列的資料為NULL,在其後存儲列資料的部分,我們會發現沒有存儲NULL,隻存儲了第1列和第4列非NULL的值。這個例子很好地說明了:不管是char還是varchar類型,NULL值是不占用存儲空間的。
Redundant是MySQL 5.0版本之前InnoDB的行記錄存儲方式,MySQL 5.0支援Redundant是為了向前相容性。Redundant行記錄以如下方式存儲:
從上圖可以看到,不同于Compact行記錄格式,Redundant行格式的首部是一個字段長度偏移清單,同樣是按照列的順序逆序放置的。當列的長度小于255位元組,用1位元組表示;若大于255個位元組,用2個位元組表示。第二個部分為記錄頭資訊(record header),不同于Compact行格式,Redundant行格式固定占用6個位元組(48位),每位的含義見表4-2。從表中可以看到,n_fields值代表一行中列的數量,占用10位,這也很好地解釋了為什麼MySQL一個行支援最多的列為1023。另一個需要注意的值為1byte_offs_flags,該值定義了偏移清單占用1個位元組還是2個位元組。最後的部分就是實際存儲的每個列的資料了。
建立一張和mytest内容完全一樣、但行格式為Redundant的表mytest2
create table mytest2 engine=innodb row_format=redundant as (select * from mytest);
show table status like 'mytest2'\G
select * from mytest2\G
現在row_format變為Redundant。同樣,通過hexdump将表空間mytest2.ibd導出到文本檔案mytest2.txt。打開檔案,找到類似如下行:
23 20 16 14 13 0c 06,逆轉為06,0c,13,14,16,20,23。分别代表第一列長度6,第二列長度6(6+6=0x0C),第三列長度為7(6+6+7=0x13),第四列長度1(6+6+7+1=0x14),第五列長度2(6+6+7+1+2=0x16),第六列長度10(6+6+7+1+2+10=0x20),第七列長度3(6+6+7+1+2+10+3=0x23)。
記錄頭資訊中應該注意48位中22~32位,為0000000111,表示表共有7個列(包含了隐藏的3列),接下去的33位為1,代表偏移清單為一個位元組。
後面的資訊就是實際每行存放的資料了,這與Compact行格式大緻相同。
請注意是大緻相同,因為如果我們來看第三行,會發現對于NULL的處理兩者是不同的。
21 9e 94 14 13 0c 06/*長度偏移清單,逆序*/
00 00 20 0f 00 74/*記錄頭資訊,固定6個位元組*/
00 00 00 2b 68 0d/*RowID*/
00 00 00 00 06 53/*TransactionID*/
80 00 00 00 32 01 10/*Roll Point*/
64/*列1資料'a'*/
00 00 00 00 00 00 00 00 00 00/*列3資料NULL*/
這裡與之前Compact行格式有着很大的不同了,首先來看長度偏移清單,我們逆序排列後得到06 0c 13 14 94 9e 21,前4個值都很好了解,第5個NULL變為了94,接着第6個列char類型的NULL值為9e(94+10=0x9e),之後的21代表14+3=0x21。可以看到對于varchar的NULL值,Redundant行格式同樣不占用任何存儲空間,因而char類型的NULL值需要占用空間。
目前表mytest2的字元集為Latin1,每個字元最多隻占用1個位元組。若這裡将表mytest2的字元集轉換為utf8,第三列char固定長度類型就不再是隻占用10個位元組了,而是10×3=30個位元組,Redundant行格式下char固定字元類型将會占據可能存放的最大值位元組數。
InnoDB Plugin引入了新的檔案格式(file format,可以了解為新的頁格式),對于以前支援的Compact和Redundant格式将其稱為Antelope檔案格式,新的檔案格式稱為Barracuda。Barracuda檔案格式下擁有兩種新的行記錄格式Compressed和Dynamic兩種。新的兩種格式對于存放BLOB的資料采用了完全的行溢出的方式,在資料頁中隻存放20個位元組的指針,實際的資料都存放在BLOB Page中,而之前的Compact和Redundant兩種格式會存放768個字首位元組。
下圖是Barracuda檔案格式的溢出行:
Compressed行記錄格式的另一個功能就是,存儲在其中的行資料會以zlib的算法進行壓縮,是以對于BLOB、TEXT、VARCHAR這類大長度類型的資料能進行非常有效的存儲。