InnoDB存储引擎文件
InnoDB存储引擎相关的文件包括重做日志文件、表空间文件。
表空间文件
InnoDB存储引擎在设计上模仿了Oracle,将存储的数据按表空间进行存放。默认配置下,会有一个初始化大小为10MB、名为ibdata1的文件,该文件就是默认的表空间文件。可以通过参数innodb_data_file_path对其进行设置,格式如下:
innodb_data_file_path = datafile_spec1[;datafile_spec2]
可以用多个文件组成一个表空间,同时指定文件的属性,如:
[mysqld]
innodb_data_file_path = /db/idbdata1:2000M;/dr2/db/ibdata2:2000M:autoextend
这里将 /db/idbdata1 和 /dr2/db/ibdata2 两个文件用来组成表空间,若这两个文件位于不同的磁盘上,则可以对性能带来一定程度的提升。两个文件的文件名后都跟了属性,表示文件idbdata1 和 ibdata2大小都为2000M,但是如果用满了这2000M后,该文件可以自动增长。
设置innodb_data_file_path参数后,对于所有基于InnoDB存储引擎的表数据都会记录到该文件内。而通过设置参数innodb_file_per_table,我们可以将每个基于InnoDB存储引擎的表单独产生一个表空间,文件名为表名.ibd。这样不用将所有数据都存放于默认的表空间中,如下所示:
表Profile、t1、t2都是InnoDB存储引擎,由于设置参数innodb_file_per_table = ON,因此产生了单独的 .ibd表空间文件。这些单独的表空间文件仅存储该表的数据、索引和插入缓冲等信息,其余信息还是存放在默认的表空间中。
重做日志文件
默认情况下会有两个文件,名称分别为ib_logfile0和ib_logfile1。重做日志文件对于InnoDB存储引擎至关重要,他们记录了对于InnoDB存储引擎的事务日志。
如果数据库由于主机掉电导致实例失败,InnoDB存储引擎会使用重做日志恢复到掉电前的时刻,以此来保证数据的完整性。
每个InnoDB存储引擎至少有一个重做日志文件组,每个文件组下至少有2个重做日志文件,如默认的ib_logfile0、ib_logfile1。为了得到更高的可靠性,可以设置多个镜像日志组。将不同的文件组放在不同的磁盘上,日志组中每个重做日志文件的大小一致,并以循环方式使用。InnoDB存储引擎先写重做日志文件1,当达到文件的最后时,会切换至重做日志文件2,当重做日志文件2也被写满时,会在切换到重做日志文件1.如下所示。
参数innodb_log_file_size 指定了重做日志文件的大小
参数innodb_log_files_in_group 指定了日志文件组中重做日志文件的数量,默认为2.
参数innodb_mirrored_log_groups 指定了日志镜像文件组的数量,默认为1.
参数innodb_log_group_home_dir指定了日志文件组所在路径。
InnoDB存储引擎的重做日志文件记录的关于每个页的更改的物理情况。
InnoDB存储引擎表类型
在InnoDB存储引擎表中,每张表都有个主键。如果在创建表时没有显示的定义主键,则InnoDB存储引擎会按照如下方式选择或者创建主键。
- 首先表中是否有非空的唯一索引,如果有,该列即为主键。
- 不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针。
InnoDB逻辑存储结构
InnoDB存储引擎所有数据都被逻辑的放在一个空间中,我们称之为表空间,表空间由段、区、页组成。如图所示
表空间
表空间可以看做是InnoDB存储引擎逻辑结构的最高层
。所有数据都是存放在表空间中。默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都放在这个表空间内,如果我们启用了参数innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。
对于启用了innodb_file_per_table的参数选项,需要注意的是,每张表的表空间内存放的只是数据、索引和插入缓冲,其他 如undo信息、系统事务信息、二次写缓冲等还是放在共享表空间内。这也就说明了即使在启用了参数innodb_file_per_table后,共享表空间还是会不断的增加大小。
段
表空间是由各个段组成的,常见的段有数据段、索引段、回滚段。InnoDB存储引擎表是索引组织的,因此数据即索引,索引即数据
。那么数据段即为B+树的页节点,索引段即为B+树的非索引节点(即non-leaf node segment)。
区
区是由64个连续的页组成,每个页的大小为16KB,即每个区的大小为1MB。对于大的数据段,InnoDB存储引擎最多每次可以申请4个区,以此来保证数据的顺序性能。
但是,在我们启用了参数innodb_file_per_table后,创建的表默认大小是96KB。区是64个连续的页,那创建的表为什么不是1MB?其实因为 在每个段开始时,先有32个页大小的碎片页来存放数据,当这些页使用完之后才是64个连续页的申请(先申请碎片页使用,用完之后开始申请连续的页)
。
页
页(也称为块)是InnoDB磁盘管理的最小单位,常见的页类型有:
- 数据页(B-Tree Node)
- Undo 页(Undo Log Page)
- 系统页(System Page)
- 事务数据页(Transaction system Page)
- 插入缓冲位图页(Insert Buffer BitMap)
- 插入缓冲空闲列表页
- 未压缩的二进制大对象页
- 压缩的二进制大对象页
行
InnoDB存储引擎是面向行的,即数据的存放按行进行存放。每个页最多允许存放16KB /2~200行的记录,即 7992行记录。
InnoDB物理存储结构
从物理意义上来看,InnoDB表由共享表空间、日志文件组(更准确地说是Redo文件组)、表结构定义文件组成
。若将innodb_file_per_table设置为on,则每个表将独立的产生一个表空间文件,以ibd结尾。数据、索引、表的内部数据字典信息都将保存在这个单独的表空间文件中。表结构定义文件以frm结尾,这个是和存储引擎无关的,任何存储引擎的表结构定义文件都是.frm文件。
InnoDB行记录格式
InnoDB存储引擎记录是以行的形式存储的,这意味着页中保存着表中一行行的数据。MySQL5.1时,InnoDB存储引擎提供了Compact、Redundant两种格式来存放行记录数据。
Compact行记录格式
Compact行记录格式设计目标是能高效存放数据。简单来说,如果一个页中存放的行数据越多,其性能就越高。Compact行记录以如下方式进行存储:
变长字段长度列表 | Null标志位 | 记录头信息 | 列1数据 | 列2数据 |
---|
Compact行格式的首部是一个非Null变长字段长度列表,当列的长度小于255字节,用1字节表示,若大于255字节,用2个字节表示,变长字段的长度最大不超过2个字节(这也是为什么varchar的最大长度为65535)。第二个部分为Null标志位,该位指示了改行数据中是否有Null值,用1表示。该部分所占的字节应该为bytes。接下来是记录头信息,用5个字节表示,最后部分就是实际存储的列数据。Null不占该部分任何数据,即Null除了占用Null标志位,实际存储不占用任何空间。另外,每行数据除了用户定义的列外,还有两个隐藏列,事务ID列和回滚指针列。若InnoDB没有定义主键,每行还会增加一个rowId列
。
示例:
执行sql语句,创建mytest表,有4个列,t1、t2、t4为varchar变长字段类型,t3为固定长度类型char。如果启用了innodb_file_per_table,打开mytest.ibd查看,否则打开共享表空间文件ibdata1。
第一行数据展示如下,变长字段长度列表逆序存放。第3列固定长度在未填充满长度时,会用ox20进行填充。
第三行有Null值,结果展示如下
Null标志位展示06,转换成二进制位00000110,为1的值即表示第2列和第3列的数据为null。在实际存储数据部分将不在存储第2、3列的值,而只存储第1、4列非null的值。
Redundant行记录格式
MySQL5.0支持Redundant行记录格式是为了向前兼容性。其行记录如下方式存储
字段长度偏移列表 | 记录头信息 | 列1数据 | 列2数据 |
---|
不同于Compact记录格式,Redundant行格式的首部是一个字段长度偏移列表。同样是按照列的顺序逆序存放。当列的长度小于255字节,用1字节表示,若大于255字节,用2个字节表示。第2个部分为记录头信息。头信息中包含n_fields值表示一行中列的数量,占用10位。也是为什么MySQL一行支持最多1023个列。最后部分就是实际存储的每个列的数据。
示例如下:
查看mytest2.ibd文件,整理得到如下内容
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、第五列长度为2,第六列长度为10.
Redundant行记录存储Null的格式和Compact有所不同,如下所示
对于varchar类型的Null值,Redudant行格式同样不占用任何存储空间,char类型的Null值需要占用空间,而Compact则都不占用空间
。
当前表mytest2的字符集为Latin1,每个字符最多占用1个字节,若将字符集修改为utf8,第三列char固定长度类型将占用10*3 = 30个字节。
行溢出数据
InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外,即作为行溢出数据。一般认为Blob、Text之类的大对象的存储会把数据存放在数据页面之外,实际上,Blob可以不将数据放在溢出页面,即使是varchar列数据,依然有可能作为行溢出数据。
之前说过,varchar类型的长度最大为65535,实际上是在Latin1字符下,如果是utf8则还要除以三,因为一个utf8字符占据3个字节,此外,还要考虑行本身的别的开销。以及65535长度是指一行中varchar列的长度总和,并不是指单一列的长度。但是,InnoDB存储引擎的页为16KB,即16384个字节,如何存放65535个字节?一般情况下,数据都是存放在B-Tree Node的页类型中,但是,当发生行溢出时,这个行溢出的页类型为Uncompress Blob Page。
如下:
可以看到一个B-tree node页类型,另外4个Uncompress Blob Page,这些页中才是真正存放了65532个字节的数据。
即然实际存放的数据都在Blob页中,为什么还有数据页。实际上,数据页中只保存了varchar(65532)的前768个字节的前缀数据,之后跟的是偏移量,指向行溢出页。因此,对于行溢出数据,其存放方式如下:
那多少长度的varchar是保存在数据页里的,多少长度的开始又保存在Blob页中。InnoDB存储引擎表是索引组织的,即B+树的结构。因此每个页中至少应该有两个行记录(否则失去了B+树的意义,变成链表了)。因此,如果当页中只有一条记录时,那么InnoDB存储引擎会自动将行数据存放到溢出页中。
如下所示:
表t的变长字段长度为9000,能放在一个页中,但是不能保证2条记录都能存放在一个页中,所以,此时是存放在Blob页中。
一般来说,阈值长度为8098,此时的行记录是放在数据页中的,而不是Blob页中。如下所示:
Compressed和Dynamic行记录格式
Compressed和Dynamic两种格式对于存放Blob数据采用了完全的行溢出方式,在数据页中只存放20个字节的指针,实际的数据都存放在Blob Page中,而之前的Compact和Redundant两个格式会存放768个前缀字节。
Compressed行记录格式的另一个功能是,存储在其中的行数据会以zlib的算法进行压缩,因此对于Blob、Text、Varchar这些大长度类型的数据能进行非常有效的存储。
MySQL行记录格式 Compact、Redundant、Dynamic、Compressed比较
1. Compact
:
- 这是最常见的行格式,默认情况下InnoDB使用的就是这种格式 ,
他对固定长度和可变长度字段进行了优化存储。对于删除的记录,会在该记录的头信息中设置一个删除标记,而不是立即从表中物理删除。
- 支持变长字段,非空字符串会有2字节的长度前缀。
2. Redundant
:
- 这是一种比较老的格式,相比Compact格式,他存储了更多的冗余信息,比如每列的类型,占用的空间比Compact格式大,但在某些情况下恢复更快。
- 同样使用删除标记来管理已删除的记录,但其记录头信息较Compact格式更为冗余。
3.Dynamic
- 动态行格式主要针对包含大量变长字段的表进行了优化,
变长字段的真正内容会被存储在行的外部,只在行内保存指向真实位置的指针。可以减少大文本字段对存储空间的影响,
但访问这些字段可能会稍微慢一些,因为需要额外的IO操作。- 只有当实际行大小超过页的最大可用空间时,才会将变长字段移到外部存储
4. Compressed
- 压缩格式对整个页面的数据进行压缩存储,可以显著减少存储空间的需求,尤其是在存储大量数据时,
适用于读取密集型而不频繁修改的场景,因为每次修改都需要解压和重新压缩。
- 支持两种压缩级别:zlib和lz4.
InnoDB数据页结构
InnoDB存储引擎管理数据库的最小磁盘单位是页。页类型为B-tree Node的页,存放的即是表中行的实际数据。
InnoDB数据页由以下七个部分组成
- File Header
- Page Header
- Infimun+Supremum Records
- User Records(行记录)
- Free Space(空闲空间)
- Page Directory(页目录)
- File Trailer(文件结尾信息)
File Header、Page Header、File Trailer的大小是固定的,用来标识该页的一些信息。其余部分为实际的行记录存储空间,大小是动态的。
File Header
File Header用来记录页的一些头信息,有如下8个部分组成。
- FIL_PAGE_OFFSET:表空间中页的偏移值。
- FIL_PAGE_PREV,FIL_PAGE_NEXT:当前页的上一个页以及下一个页。B+ Tree特性决定了叶子节点必须是双向列表。
- FIL_PAGE_LSN: 代表该页最后被修改的日志序列位置LSN
- FIL_PAGE_TYPE:页的类型,其中0x45BF代表了存放的数据页。
页类型包括:B+ Tree Node、Undo Log页、索引节点、Insert Buffer空闲列表、该页为最新分配、Insert Buffer位图、系统页、BloB页等。
Page Header
接着File Header部分的是Page Header,用来记录数据页的状态信息。其中关键部分如下
- PAGE_N_DIR_SLOTS: 在Page Directory中的slot数。
- PAGE_HEAP_TOP: 堆中第一个记录的指针。
- PAGE_FREE: 指向空闲列表的首指针。
- PAGE_GARBAGE: 已删除记录的字节数
- PAGE_LAST_INSERT:最后插入记录的位置。
- PAGE_DIRECTION: 最后插入的方向。
- PAGE_N_DIRECTION: 一个方向连续插入记录的数量
- PAGE_N_RECS: 该页中记录的数量
- PAGE_MAX_TRX_ID: 修改当前页的最大事务ID
- PAGE_LEVEL:当前页在索引树中的位置,0x00代表页节点
- PAGE_INDEX_ID: 当前页属于哪个索引ID
Infimun+Supremum Records
在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimun记录是比该页中任何主键值都要小的值。Supremum值比任何可能大的值还要大的值。这两个值在创建时被建立,并且在任何情况下不会被删除。
User Records和FreeSpace
User Records 是实际存储行记录的内容。InnoDB存储引擎表总是B+树索引组织的。
Free Space指的就是空闲空间,同样也是链表数据结构,当一条记录被删除后,该空间会被加入空闲链表中。
File trailer
为了保证页能够完整的写入磁盘,InnoDB存储引擎的页中设置了File trailer部分,只有一个FIL_PAGE_END_LSN部分,占用8个字节,前4个字节代表该页的checksum值,最后4个字节和File Header中的FIL_PAGE_LSN相同,通过这两个值来和File_PAGE_SPACE_OR_CHECKSUM以及FIL_PAGE_LSN进行比较,看是否一致,以此来保证页的完整性。
分区表
分区概述
分区功能并不是在存储引擎层完成的,MySQL数据库在5.1版本时添加了对于分区的支持,这个过程是将一个表或者索引物理的分为多个更小、更可管理的部分。就应用而言,从逻辑上讲,只有一个表或者一个索引,但是在物理上这个表或者索引可能由数十个物理分区组成。每个分区都是独立的对象,可以独自处理,也可以作为一个更大对象的一部分进行处理。
MySQL数据库支持的分区类型为水平分区,并不支持垂直分区。此外,MySQL数据库的分区是局部分区索引,一个分区中既存放了数据又存放了索引。
当前MySQL数据库支持以下几种类型的分区:
- RANGE分区:行数据基于属于一个给定连续区间的列值放入分区。
- LIST分区:和Range分区类似,只是List分区面向的是离散的值。
- Hash分区:根据用户自定义的表达式的返回值来进行分区,返回值不能为负数。
- Key分区:根据MySQL数据库提供的哈希函数来进行分区。
不论创建何种类型的分区,如果表中存在主键或者是唯一索引时,分区列必须是唯一索引的一个组成部分
。唯一索引可以是允许Null值,并且分区列只要是唯一索引的一个组成部分,不需要整个唯一索引列都是分区列。如下所示
当建表时没有指定主键,唯一索引时,可以指定任何一个列为分区列。
Range分区
最常用的一种分区类型。如下所示:
当数据id小于0时,插入p0分区,当id大于等于10小于20时,插入p1分区。启用分区之后,表不再由一个ibd文件组成。
表根据列id分区,因此数据是根据id列的值的范围存放在不同的物理文件中。当插入一个不在分区中定义的值时,MySQL数据库会抛出一个异常。对于上述问题,可以对分区添加一个maxvalue值的分区,maxvalue可以理解为正无穷,因此所有大于等于20并且小于maxvalue的值放入p2分区。
Range分区主要用于日期列的分区,如下,根据年来分区存放销售记录。
如果要删除2008年的数据,只需要删除2008年数据所在分区即可。
另一个好处是,可以加快某些查询操作。SQL优化器只需要去搜索p2008分区,而不会去搜索其他分区,因此大大提高了执行的速度。
如何按照月来进行分区划分,如下
但是在执行上述SQL的时候会发现,优化器不会根据分区进行选择,如下
可以看到优化器对于分区p201001、p201002、p201003都进行了搜索,产生这个问题的主要原因是,对于Range分区的查询,优化器只能对YEAR()、TO_DAYS()、TO_SECONDS()、UNIX_TIMESTAMP()这一类函数进行优化选择,因此,需要将分区函数改为如下形式:
此时,在进行相同的查询,优化器就可以对特定的分区进行查询了。
LIST分区
LIST分区和Range分区非常相似,只是分区列的值是离散的,而非连续的。如:
不同于Range分区中定义的Values Less Than。List分区使用Values In,所以每个分区的值都是离散的,只能是定义的值。
Hash分区
Hash分区的目的是将数据均匀的分布到预先定义的各个分区中,保证各分区的数据数量大致都是一样的。
在Range和List分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中。
要使用Hash分区来分割一个表,要在create table语句上添加一个"partition by hash(expr)"子句,其中"expr"是一个返回一个整数的表达式,此外,还需要添加一个"partitions num"子句,表示要分割成的分区数量。如果不加,默认分区数量为1.
如下:
如果将一个列为2010-04-01这条记录插入表t_hash中,那么保存这条记录的分区确定如下:
bash
mod(year('2010-04-01'),4)
= mod(2010,4)
= 2
因此,会放入2号分区。
Key分区
Key分区和Hash分区相似,不同在于,Hash分区使用用户定义函数进行分区,Key分区使用MySQL提供的函数进行分区
。MySQL数据库使用内部的哈希函数,这些函数是基于与password()一样的运算法则。如
Columns分区
前面所述的 Range、List、Hash、Key四种分区中,分区的条件必须是整型
。如果不是整型,那么需要通过函数将其转化成整型,如year()、to_days()、month()等函数。Columns分区可以直接使用非整型的数据进行分区,分区根据类型直接比较而得,不需要转化为整型。
Columns分区支持以下数据类型
- 整形类型:如int、smallint、tinyint、bigint。float、decimal不予支持
- 日期类型:如date和datetime,其余日期类型不予支持
- 字符串类型:char、varchar、binary、varbinary。blob和text不予支持
对于日期类型的分区,不需要再使用函数year()、to_days()了,而直接可以使用columns,如
此外,还可以使用多个列进行分区,如下
子分区
子分区是在分区的基础上再进行分区,有时也称这种分区为复合分区。MySQL允许在Range和List分区上再进行Hash或者是Key的子分区,如:
表ts先根据b列进行了Range分区,然后又再进行了一次Hash分区,所以分区的数量应该为3*2 = 6个。子分区可以用于特别大的表,在多个磁盘间分别分配数据和索引,假设有6个磁盘,分别为/disk0、/disk1、/disk2等
示例如下:
但是InnoDb存储引擎会忽略Data Directory和Index Directory的语法,因此上述分区表的数据和索引文件分开放置对InnoDb存储引擎是无效的。
InnoDB存储引擎的文件路径位置
InnoDB存储引擎在MySQL中的行为和MyISAM或一些其他存储引擎有所不同,特别是在处理Data Directory和Index Directory上,在
MySQL配置中,通常可以使用两种语法来指定表的数据文件和索引文件存放的自定义路径,但这主要适用于使用文件系统存储引擎如MyISAM的表
对于InnoDB存储引擎,他有自己的表空间管理机制,
InnoDB默认使用共享表空间(通常为ibdata1文件)存放数据和索引。如果启用了innodb_per_file_table配置选项后,则每个InnoDB表的数据和索引会存储在各自的.ibd文件中,这些文件默认位于MySQL的数据目录下, 重要的是,InnoDB引擎并不直接使用Data Directory和Index Directory选项来改变单个表的数据或索引文件的存放位置。
分区中的Null值
MySQL数据库的分区总是把Null值视为小于任何一个非null值,这和MySQL数据库中对于Null的Order By的排序是一样的。
对于Range分区,如果对于分区列插入了Null值,则MySQL数据库会将该值放入最左边的分区。
List分区下使用Null值,则必须显示的指定哪个分区中存放Null值,否则会报错。
Hash和Key分区对于Null的处理方式是将含有Null值的记录返回为0.
分区和性能
数据库的应用分为两类:OLTP、OLAP.
对于OLAP的应用,分区可以很好的提高查询的性能
,因为OLAP应用大多数查询需要频繁的扫描一张很大的表。假设有一张1亿行的表,其中有一个时间戳属性列,查询需要从这张表中获取一年的数据,如果按时间戳进行分区,则只需要扫描相应的分区即可。
对于OLTP应用,不可能会获取一张大表中10%的数据,大部分都是通过索引返回几条记录即可。根据B+树原理可知。对于一张大表,一般的B+树需要2~3次磁盘IO。因此,B+树可以很好的完成操作,不需要分区的帮助。
考虑如下场景:假设有一张1000w行的表,基于主键做10个Hash分区。则每个分区包含100w条记录。
- 场景一:根据主键查询,100w行和1000w行的数据本身构成的B+树层次都是一样的,并不会带来性能的提升。
- 场景二:根据其他非分区列查询,则需要扫描全部的10个分区,假设每个分区B+树高度为2,则共计需要20次磁盘IO,而对于单表设计,往往只需要2~3次IO。
因此,设计时要考虑访问模式,对于OLTP应用的表,不当的使用分区会导致性能显著下降。