InnoDB存储引擎和大多数数据库一样,记录是以行的形式存储的。也就是说页中保存着表中一行行的数据。在InnoDB 1.0.x版本之前,InnoDB存储引擎
提供了Compact和Redundant两种格式来存放行记录数据。
Redundant格式是为了兼容之前版本而保留的,如果阅读过InnoDB的源代码,用户会发现源代码中是用PHYSICAL RECORD(NEW STYLE)和PHYSICAL RECORD(OLD STYLE)来区分两种格式。
在MySql 5.1版本中,默认设置为Compact行格式。用户可以通过下面命令查看表使用的行格式,其中row_format属性表示当前所使用的行记录结构类型。
mysql> show table status like 'test'\G
*************************** 1. row ***************************
Name: test
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 1
Avg_row_length: 16384
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 5242880
Auto_increment: NULL
Create_time: 2016-07-20 16:13:43
Update_time: NULL
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
可以看到test表是Compact的行格式。数据库实例的作用之一就是读取页中存放的行记录。如果用户自己知道页中行记录的组织规则,也可以自行通过编写工具的方式来读取其中的记录。
1、Compact行记录格式
Compact行记录是在MySql 5.0中引入的,其设计目标是高效地存储数据。简单来说,一个页中存放的行数据越多,其性能就越高。下面是Compact行记录的存储方式:
Compact行记录格式的首部是一个非NULL变长字段长度列表,并且其是按照列的顺序逆序放置的,其长度是:
(1)若列的长度小于255字节,用1字节表示;
(2)若大于255个字节,用2字节表示;
变长字段的长度最大不可以超过2字节,这是因为在MySql数据中varchar类型的最大长度限制为65535。
变长字段之后的第二部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示。该部分所占的字节应该为1字节。
接下来的部分是记录头信息(record header),固定占用5字节(40位),每位的含义如下图所示:
最后的部分是实际存储每个列的数据。NULL不占该部分任何空间,即NULL除了占有NULL标志位,实际存储不占有任何空间。
另外,每行数据除了用户定义的列外,还有两个隐藏列,事务ID列和回滚指针列,分别为6字节和7字节的大小。若InnoDB表没有定义主键,每行还会增加一个6字节的rowid列。
接下来用一个具体示例来分析Compact行记录的内部结构:
mysql> create table mytest(
-> t1 varchar(10),
-> t2 varchar(10),
-> t3 char(10),
-> t4 varchar(10)
-> )engine=innodb charset=latin1 row_format=compact;
Query OK, 0 rows affected (0.09 sec)
下面插入数据:
mysql> insert into mytest values('a','bb','bb','ccc');
Query OK, 1 row affected (0.02 sec)
mysql> insert into mytest values('d', 'ee', 'ee', 'fff');
Query OK, 1 row affected (0.01 sec)
mysql> insert into mytest values('d', NULL, NULL, 'fff');
Query OK, 1 row affected (0.01 sec)
查询表中的数据:
mysql> select * from mytest\G
*************************** 1. row ***************************
t1: a
t2: bb
t3: bb
t4: ccc
*************************** 2. row ***************************
t1: d
t2: ee
t3: ee
t4: fff
*************************** 3. row ***************************
t1: d
t2: NULL
t3: NULL
t4: fff
3 rows in set (0.00 sec)
如果启用了参数innodb_file_per_table,则打开表空间文件mytest.ibd。否则,打开默认的共享表空间文件ibdata1,本文中没有启用参数innodb_file_per_table,所有打开文件ibdata1.
在Linux下,首先找到ibdata1文件的位置。
root@TryHard:~# find / -name ibdata1;
/var/lib/mysql/ibdata1
然后,将二进制文件ibdata1重定向为mytest.txt文件,然后打开mytest.txt文件。
root@TryHard:~# hexdump -C -v /var/lib/mysql/ibdata1 > mytest.txt
找到如下图所示的内容:
下面是对第一行内容进行分析:
03 02 01 是变长字段长度列表,t1,t2,t3字段是变长字段,逆序显示这三个字段的长度。插入的数据中t3的值为3个字节,t2的值为2个字节,t1的值为1个字节。
00 是NULL标志位,为0表示第一行没有NULL值。
00 00 10 00 2c 是Record Header,固定5字节长度。
00 00 00 00 09 00 是InnoDB自动创建的rowID,6字节长度。
两个隐藏列,事务ID列和回滚指针列,分别为6字节和7字节的大小。
00 00 00 00 1e 09 是事务ID列,6字节长度。
8a 00 00 01 3d 01 10 是回滚指针列,7字节长度。
61是t1列的数据’a’;
62 62是t2列的数据’bb’;
62 62 20 20 20 20 20 20 20 是t3列的数据’bb’,t2是固定长度char字段,如果未用完其占用的长度空间时,会用0x20来进行填充。
63 63 63是t4列的数据’ccc’;
Record Header的值是00 00 10 00 2c,最后两个字节是00 2c,这两个字节代表next_recorder,0x2c代表下一个记录的偏移量,即当前记录的位置加上偏移量0x2c就是下一条记录的起始位置。所以InnoDB存储引擎在页内部是通过一种链表的结构来串连各个行记录的。
第二行和第一行类似 。
下面是第三行的内容:
03 01 变长字段长度列表,逆序;t1长度为1,t4长度为3,t2和t3为NULL。
06 NULL标志位,第三行有NULL值。
00 00 20 ff 98 是Record Header;
00 00 00 00 09 02是RowId,6个字节;
00 00 00 00 1e 0b是事务ID列,6个字节;
8c 00 00 02 1d 01 10是回滚指针列,7个字节。
64是t1字段的值’d’;
66 66 66是t4字段的值’f’;
第三行有NULL值,因为NULL标志位不再是00而是06,转换成二进制00000110,为1的值表示第2列和第3列的数据为NULL。其后存储列数据的部分,没有存储NULL列,而只存储了第1列和第4列的非NULL的值,这也说明了,不管是char还是varchar类型,在compact格式下NULL值都不占用任何存储空间。
2、Redundant行记录格式
Redundant是MySql 5.0版本之前InnoDB的行记录存储方式,MySql 5.0支持Redundant是为了兼容之前版本的页格式。Redundant行记录采用下图所示的方式存储。
Redundant行记录格式的首部是一个字段长度偏移列表,同样是安装列的顺序逆序存放的。若列的长度小于255字节,用1字节表示;若大于255字节,用2字节表示。第二个部分为记录头信息(record header),不同于Compact行记录格式,Redundant行记录格式的记录头占用6字节,每位的含义如下图所示:
其中,n_fields的值代表一行中列的数量,占用10位,所以能表示的最大值为1023,这也解释了为什么MySql数据库一行支持的最多的列数量为1023。另一个需要注意的值为1byte_offs_flag,该值定义了偏移列表占用1字节还是2字节。
最后部分就是实际存储的每个列的数据了。
创建一个Redundant表mytest2:
mysql> create table mytest2 engine=innodb row_format=redundant as select * from mytest;
Query OK, 3 rows affected (0.04 sec)
Records: 3 Duplicates: 0 Warnings: 0
查看表mytest2信息:
mysql> show table status like 'mytest2'\G
*************************** 1. row ***************************
Name: mytest2
Engine: InnoDB
Version: 10
Row_format: Redundant
Rows: 3
Avg_row_length: 5461
Data_length: 16384
Max_data_length: 0
Index_length: 0
Data_free: 5242880
Auto_increment: NULL
Create_time: 2017-03-20 15:46:57
Update_time: NULL
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options: row_format=REDUNDANT
Comment:
1 row in set (0.00 sec)
查看数据:
mysql> select * from mytest2\G
*************************** 1. row ***************************
t1: a
t2: bb
t3: bb
t4: ccc
*************************** 2. row ***************************
t1: d
t2: ee
t3: ee
t4: fff
*************************** 3. row ***************************
t1: d
t2: NULL
t3: NULL
t4: fff
3 rows in set (0.00 sec)
将二进制文件ibdata1重定向为mytest.txt文件,然后打开mytest.txt文件。
root@TryHard:~# hexdump -C -v /var/lib/mysql/ibdata1 > mytest.txt
第一行数据:
23 20 16 14 13 0c 06 是长度偏移列表,逆序。
00 00 10 0f 00 ba 是Record Header,固定6个字节;
00 00 00 00 09 0c 是rowId列,6个字节。
00 00 00 00 1e 1b 是事务ID列,6个字节。
9a 00 00 01 4b 01 10 是回滚指针列,7个字节。
61 是t1列数据’a’;
62 62是t2列数据’bb’;
62 62 20 20 20 20 20 20 20 20 是t3列数据’bb’, char类型;
63 63 63是t4列数据’ccc’;
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).
第1列指的是rowID列,第2列指的是事务ID列,第3列指的是回滚指针列,第4列指的是t1,……
在接下来的记录头信息(record Header)中应该注意到48位中的第22-32位,为0000000111,表示表共有7个列(包含了隐藏的3列),接下来的第33位为1,大表偏移列表为一个字节。
后面的信息就是实际存放的数据。
第三行数据:
21 9e 94 14 13 0c 06 长度偏移列表,逆序;
00 00 20 0f 00 74 是Record Header,固定长度为6字节;
00 00 00 00 09 0e是RowId列;
00 00 00 00 1e 1b 是事务id,6个字节;
9a 00 00 01 4b 01 2e 是回滚指针列,7 个字节;
64是列t1的数据’d’;
t2类型是varchar,并且值为NULL,所以t2的值不占用存储空间。
00 00 00 00 00 00 00 00 00 00 是t3的数据NULL,t3是char类型;
66 66 66是列t4的数据’fff’;
长度偏移列表逆序之后得到 06, 0c, 13, 14, 94, 9e, 21,前4个值都好理解。
第5个NULL值变为了94(该列是t2字段),第6个char类型的NULL值为9e(94+10=0x9e)(该列是t3的字段),之后的21大表(14+3 = 0x21)。可以看出对于varchar类型的NULL值,Redundant行记录格式同样不占用任何存储空间,而char类型的NULL值需要占用空间。
当前表mytest2的字符集为Latin1,每个字符最多只占用1个字节。若用户将表mytest2的字符集转换为utf8,第三列char固定长度类型不再是只占用10字节了,而是10*3 = 30字节。