物理存储结构
从物理意义上来看,InnoDB表由共享表空间、日志文件组(更准确地说,应该是Redo文件组)、表结构定义文件组成。若将innodb_file_per_table设置为on,则每个表将独立地产生一个表空间文件,以ibd结尾,数据、索引、表的内部数据字典信息都将保存在这个单独的表空间文件中。表结构定义文件以frm结尾,这个是与存储引擎无关的,任何存储引擎的表结构定义文件都一样,为.frm文件。
逻辑存储结构
nnoDB存储引擎的逻辑存储结构,所有数据都被逻辑地存放在一个空间中,我们称之为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中有时也称为块(block),1 extent = 64 pages,InnoDB存储引擎的逻辑存储结构大致如图所示。
表空间
表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都是存放在表空间中。默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都放在这个表空间内。如果我们启用了参数innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。
对于启用了innodb_file_per_table的参数选项,需要注意的是,每张表的表空间内存放的只是数据、索引和插入缓冲bitmap页,其他类的数据,如撤销(Undo)信息、系统事务信息、二次写缓冲(double write buffer)等还是存放在原来的共享表空间内。这也就说明了另一个问题:即使在启用了参数innodb_file_per_table之后,共享表空间还是会不断地增加其大小。
InnoDB存储引擎不会在rollback时去收缩这个表空间。虽然InnoDB不会帮你回收这些空间,但是MySQL会自动判断这些Undo信息是否需要,如果不需要,则会将这些空间标记为可用空间,供下次Undo使用。
段
上图中显示了表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。InnoDB存储引擎表是索引组织的(index organized),因此数据即索引,索引即数据。那么数据段即为B+树的页节点(上图的leaf node segment),索引段即为B+树的非索引节点(上图的non-leaf node segment)。
区
区是由64个连续的页组成的,每个页大小为16KB,即每个区的大小为1MB。对于大的数据段,InnoDB存储引擎最多每次可以申请4个区,以此来保证数据的顺序性能。
在我们启用了参数innodb_file_per_talbe后,创建的表默认大小是96KB。区是64个连续的页,那创建的表的大小至少是1MB才对啊?其实这是因为在每个段开始时,先有32个页大小的碎片页(fragment page)来存放数据,当这些页使用完之后才是64个连续页的申请。
页
同大多数数据库一样,InnoDB有页(page)的概念(也可以称为块),页是InnoDB磁盘管理的最小单位。InnoDB页的大小(16KB),且不可以更改(也许通过更改源码可以)。
常见的页类型有:
- 数据页(B-tree Node)。
- Undo页(Undo Log Page)。
- 系统页(System Page)。
- 事务数据页(Transaction system Page)。
- 插入缓冲位图页(Insert Buffer Bitmap)。
- 插入缓冲空闲列表页(Insert Buffer Free List)。
- 未压缩的二进制大对象页(Uncompressed BLOB Page)。
- 压缩的二进制大对象页(Compressed BLOB Page)。
行
InnoDB存储引擎是面向行的(row-oriented),也就是说数据的存放按行进行存放。每个页存放的行记录也是有硬性定义的,最多允许存放16KB/2~200行的记录,即7992行记录。
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就代表了当前使用的行记录结构类型。
Compact行记录格式
一.变长字段长度列表:
是按照列的顺序逆序放置的。当列的长度小于255字节,用1字节表示,若大于255个字节,用2个字节表示,变长字段的长度最大不可以超过2个字节(这也很好地解释了为什么MySQL中varchar的最大长度为65 535,因为2个字节为16位,即216=1=65 535)。
二.NULL标志位
该位指示了该行数据中是否有NULL值,用1表示。该部分所占的字节应该为bytes。
如果所有字段都 NOT NULL, 这部份长度为0 byte;
如果1-8个字段都为 NULL,这部份长度为1 byte;
如果N个字段都为 NULL,这部份长度为N/8 byte
三.是为记录头信息(record header),固定占用5个字节(40位),每位的含义见下表。
记录头信息的最后4个字节代表next_recorder,0x6800代表下一个记录的偏移量,当前记录的位置+0x6800就是下一条记录的起始位置。所以InnoDB存储引擎在页内部是通过一种链表的结构来串联各个行记录的
四.实际存储的每个列的数据
需要特别注意的是,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');
03 02 01/*变长字段长度列表,逆序*/ 第一个varchar 1byte 第二个2byte,第三个3byte
00/*NULL标志位,第一行没有NULL值*/ 转为2进制是0000 0000,因为只有4个字段,只有后面0000起作用
00 00 10 00 2c/*记录头信息,固定5字节长度*/
00 00 00 2b 68 00/*RowID我们建的表没有主键,因此会有RowID*/
00 00 00 00 06 05/*TransactionID*/
80 00 00 00 32 01 10/*Roll Pointer*/
61/*列1数据'a' 长度01 */
62 62/*列2'bb' 长度02*/
62 62 20 20 20 20 20 20 20 20/*列3数据'bb'*/
63 63 63/*列4数据'ccc' 长度03*/
insert into mytest values('d',NULL,NULL,'fff');
03 01/*变长字段长度列表,逆序*/第一个varchar(‘d’)1byte 第二个('fff')3byte
06/*NULL标志位,第三行有NULL值*/转为2进制是0000 0110,只有后面0110起作用,表示第二,三为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'*/
Redundant行记录格式
Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式,MySQL 5.0支持Redundant是为了向前兼容性。详情参考《Mysql技术内幕》。
Char的行结构存储
通常的理解VARCHAR是存储变长长度的字符类型,CHAR是存储定长长度的字符类型。从MySQL 4.1开始,CHR(N)中的N指的是字符的长度,而不是之前版本的字节长度。那也就是说,在不同的字符集下,CHAR的内部存储的不是定长的数据。
比如UTF-8下CHAR(10)最小可以存储10个字节的字符,而最大可以存储30个字节的字符。
在InnoDB存储引擎内部对于CHAR类型在多字节字符集类型的存储了,CHAR很明确地被视为了变长类型,对于未能占满长度的字符还是填充0x20。内部对于字符的存储和我们用hex函数看到的也是一致的。我们可以说,在多字节字符集的情况下,CHAR和VARCHAR的行存储基本是没有区别的。
- 对于InnoDB表,因为它的数据行内部存储格式对固定长度的数据行和可变长度的数据行不加区分(所有数据行共用一个表头部分,这个标头部分存放着指向各有关数据列的指针),所以使用char类型不见得会比使用varchar类型好。事实上,因为char类型通常要比varchar类型占用更多的空间,所以从减少空间占用量和减少磁盘i/o的角度,使用varchar类型反而更有利;
- 存储很短的信息,比如门牌号码101,201……这样很短的信息应该用char,因为varchar还要占个byte用于存储信息长度,本来打算节约存储的现在得不偿失。
- 固定长度的。比如使用uuid作为主键,那用char应该更合适。因为他固定长度,varchar动态根据长度的特性就消失了,而且还要占个长度信息。
- 十分频繁改变的column。因为varchar每次存储都要有额外的计算,得到长度等工作,如果一个非常频繁改变的,那就要有很多的精力用于计算,而这些对于char来说是不需要的。
行溢出数据
从变长字段长度列表,应该理解VARCHAR(N)中,N指的是字符的长度,VARCHAR类型最大支持65 535指的是65 535个字节。
此外,MySQL官方手册中定义的65 535长度是指所有VARCHAR列的长度总和,如果列的长度总和超出这个长度,依然无法创建。这是因为还有别的开销,因此实际能存放的长度为65 532。
即使我们能存放65 532个字节了,但是有没有想过,InnoDB存储引擎的页为16KB,即16 384个字节,怎么能存放65 532个字节呢?一般情况下,数据都是存放在B-tree Node的页类型中,但是当发生行溢处时,则这个存放行溢处的页类型为Uncompress BLOB Page。
Compact和Redundant两种格式,称为Antelope文件格式。会存放768个前缀字节。
Compressed与Dynamic行记录格式
InnoDB Plugin引入了新的文件格式(file format,可以理解为新的页格式),对于Antelope文件格式,新的文件格式称为Barracuda。Barracuda文件格式下拥有两种新的行记录格式Compressed和Dynamic两种。新的两种格式对于存放BLOB的数据采用了完全的行溢出的方式,在数据页中只存放20个字节的指针,实际的数据都存放在BLOB Page中。