-
表结构定义文件
MySQL数据的存储是根据表进行的,每个表都会有与之对应的文件。但不论表采用何种存储引擎,MySQL都有一个以frm为后缀名的文件,这个文件记录了该表的表结构定义。frm还用来存放视图的定义,如用户创建了一个v_a视图,那么对应的会产生一个v_a.frm文件,用来记录视图的定义。
查看MySQL 数据文件存储位置的方法:
-
表空间文件 (InnoDB)
InnoDB在默认配置下会初始化一个大小为10MB(可以自动增加大小),名为ibdata1的文件作为默认的表空间文件,用户可以通过innodb_data_file_path 对其进行设置,设置innodb_data_file_path参数之后,所有基于InnoDB存储引擎的表的数据都会记录到该共享表空间中。若设置了参数innodb_file_per_table, 则用户可以将每个基于InnoDB存储引擎的表产生一个独立的表空间。命名规则为:表名.ibd。需要注意的是,单独的表空间文件仅存储该表的数据、索引和插入缓冲BITMAP等信息,其余信息(如回滚信息,插入缓冲索引页,系统事务信息,二次写缓冲等)还是存放在默认的表空间中。
表空间又有段(Segment),区(extent),页(page)组成。
- 段
常见的段有数据段(B+树的叶子节点)、索引段(B+树非索引节点)、回滚段等。
- 区
区是由连续的页组成的空间,在任何情况下区的大小都为1MB。默认情况下InnoDB存储引擎页的大小为16KB,即一个区中一共有64个连续的页。
- 页
页是InnoDB磁盘管理的最小单位,默认页的大小为16KB,可以通过innodb_page_size参数将页的大小设置为4K、8K、16K。
InnoDB存储引擎中,常见的页类型有:
- 数据页(B-tree Node)
- undo页(undo Log Page)
- 系统页(System Page)
- 事务数据页(Transaction system Page)
- 插入缓冲位图页(Insert Buffer Bitmap)
- 插入缓冲空闲列表页(Insert Buffer Free List)
- 未压缩的二进制大对象页(Uncompass BLOB Page)
- 压缩的二进制大对象页(compassed BLOB Page)
- 行
InnoDB存储引擎提供了Compact 和Redundant两种格式来存放记录数据,Redundant格式是为了兼容之前的版本而保留的。默认的行格式为Compact。我们可以通过show table status like 'table_name' 来查看当前表使用的行格式。
我们可以在创建或修改表的语句中指定行格式:
CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
ALTER TABLE 表名 ROW_FORMAT=行格式名称
- Compact行记录格式
我们知道MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、各种TEXT类型,各种BLOB类型,我们也可以把拥有这些数据类型的列称为变长字段,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,这样才不至于把MySQL服务器搞懵,所以这些变长字段占用的存储空间分为两部分:
1、真正的数据内容
2、占用的字节数
在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放,我们再次强调一遍,是逆序存放。其长度为:
若列的长度小于255字节,用1字节表示。
若大于255个字节,用2个字节表示。
变长字段的长度最大不可以超过2个字节,因为MYSQL数据库中VARCHAR类型的最大长度限制为65535。(并不是所有记录都有这个 变长字段长度列表 部分,比方说表中所有的列都不是变长的数据类型的话,这一部分就不需要有)。
2、NULL标志位
如果表中没有允许存储 NULL 的列,则 NULL值列表 也不存在了,否则将每个允许存储NULL的列对应一个二进制位,二进制位按照列的顺序逆序排列,二进制位表示的意义如下:
二进制位的值为1时,代表该列的值为NULL。
二进制位的值为0时,代表该列的值不为NULL。
MySQL规定NULL值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0。如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。
3、记录头信息
固定占用5个字节(40位)
- 隐藏列
除了数据列之外,InnoDB还会为每一列增加隐藏列
补充:
transaction_id (DB_TRX_ID) :记录每一行最近一次修改它的事务ID,
roll_pointer (DB_ROLL_PTR):指向当前记录项的undo信息。
两者跟行记录回滚和MVCC实现相关(后面介绍)。
-
Redundant行格式
- 字段长度偏移列表
注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:
1、没有了变长两个字,意味着Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表。
2、 多了个偏移两个字,这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。
- 记录头信息
Redundant行格式的记录头信息占用6字节,48个二进制位,这些二进制位代表的意思如下:
- delete_mask
这个属性标记着当前记录是否被删除,占用1个二进制位,值为0的时候代表记录并没有被删除,为1的时候代表记录被删除掉了。
被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后把其他的记录在磁盘上重新排列需要性能消耗,所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的垃圾链表,在这个链表中的记录占用的空间称之为所谓的可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。(其实插入的时候如果是按照索引的递增顺序插入的,那么数据是紧凑的,如果数据是随机插入的,就可能会造成索引的数据页分裂,空间不能完全利用)。
如果想回收已删除数据的磁盘空间,可以通过alter table A engine=InnoDB命令来重建表完成。也就是MYSQL会自动创建一个临时表B,然后将A中的数据导入到表B中,然后删除旧表A并将B表名改为A。
- min_rec_mask
B+树的每层非叶子节点中的最小记录都会添加该标记,记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录。
- n_owned
InnoDB将所有正常的记录(包括最大和最小记录,不包括标记为已删除的记录)划分为几个组。每个组的最后一条记录(也就是组内最大的那条记录)的头信息中的n_owned属性表示该记录拥有多少条记录,也就是该组内共有几条记录。将每个组的最后一条记录的地址偏移量单独提取出来按顺序存储到靠近页的尾部的地方,这个地方就是所谓的Page Directory,也就是页目录。页面目录中的这些地址偏移量被称为槽(英文名:Slot),所以这个页面目录就是由槽组成的。
- heap_no
这个属性表示当前记录在本页中的位置,从2开始,因为0、1分别记录着最小记录和最大记录。即 Infimum 和 Supremum。
- record_type
这个属性表示当前记录的类型,一共有4种类型的记录,0表示普通记录,1表示B+树非叶节点记录,2表示最小记录,3表示最大记录。我们自己插入的记录就是普通记录,它们的record_type值都是0,而最小记录和最大记录的record_type值分别为2和3。
- next_record
它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量。比方说第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。这其实是个链表,可以通过一条记录找到它的下一条记录。但是需要注意注意再注意的一点是,下一条记录指得并不是按照我们插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。而且规定 Infimum记录(也就是最小记录) 的下一条记录就本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)。
-
InnoDB数据页结构
InnoDB数据页由以下7部分组成:
- File Header (文件头 38字节)
- Page Header (页头 56字节)
- Infimun 和 Supremum Records(最小记录和最大记录)
- User Records(用户记录,即行记录)
- Free Space(空闲空间)
- Page Directory(页目录)
- File Trailer(文件结尾信息 8字节)
-
File Header
FIL_PAGE_SPACE_OR_CHKSUM
这个代表当前页面的校验和(checksum)。啥是个校验和?就是对于一个很长很长的字节串来说,我们会通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和。这样在比较两个很长的字节串之前先比较这两个长字节串的校验和,如果校验和都不一样两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗。
FIL_PAGE_OFFSET
每一个页都有一个单独的页号,就跟你的身份证号码一样,InnoDB通过页号来可以唯一定位一个页。
-
Page Header
名称 | 占用空间大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS |
2 字节 |
在页目录中的槽数量 |
PAGE_HEAP_TOP |
2 字节 |
还未使用的空间最小地址,也就是说从该地址之后就是Free Space |
PAGE_N_HEAP |
2 字节 |
本页中的记录的数量(包括最小和最大记录以及标记为删除的记录) |
PAGE_FREE |
2 字节 |
第一个已经标记为删除的记录地址(各个已删除的记录通过next_record 也会组成一个单链表,这个单链表中的记录可以被重新利用) |
PAGE_GARBAGE |
2 字节 |
已删除记录占用的字节数 |
PAGE_LAST_INSERT |
2 字节 |
最后插入记录的位置 |
PAGE_DIRECTION |
2 字节 |
记录插入的方向 |
PAGE_N_DIRECTION |
2 字节 |
一个方向连续插入的记录数量 |
PAGE_N_RECS |
2 字节 |
该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录) |
PAGE_MAX_TRX_ID |
8 字节 |
修改当前页的最大事务ID,该值仅在二级索引中定义 |
PAGE_LEVEL |
2 字节 |
当前页在B+树中所处的层级 |
PAGE_INDEX_ID |
8 字节 |
索引ID,表示当前页属于哪个索引 |
PAGE_BTR_SEG_LEAF |
10 字节 |
B+树叶子段的头部信息,仅在B+树的Root页定义 |
PAGE_BTR_SEG_TOP |
10 字节 |
B+树非叶子段的头部信息,仅在B+树的Root页定义 |
-
槽
Page Directory (页目录)中存放了记录的相对位置(注意,这里存放的是相对位置,而不是偏移量),有些时候这些记录指针称为Slots(槽)或者目录槽(Directory Slots)。
InnoDB对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录,最大记录所在的分组拥有的记录条数只能在 1~8 条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间。所以分组是按照下边的步骤进行的:
- 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组。
- 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个。
- 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录。这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量。
-
File Trailer
我们知道InnoDB存储引擎会把数据存储到磁盘上,但是磁盘速度太慢,需要以页为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中。但是在同步了一半的时候中断电了咋办,这不是莫名尴尬么?为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一半的尴尬情况),InnoDB在每个页的尾部都加了一个File Trailer部分,这个部分由8个字节组成,可以分成2个小部分:
前4个字节代表页的校验和
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trialer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
后4个字节代表页面被最后修改时对应的日志序列位置(LSN)
这个部分也是为了校验页的完整性的,只不过我们目前还没说LSN是个什么意思,所以大家可以先不用管这个属性。
这个File Trailer与FILE Header类似,都是所有类型的页通用的。