逻辑存储结构
在InnoDB存储引擎中,所有数据都存放在表空间(tablespace
)中,表空间由段(segment
)、区(extent
)、页(page
)、行(Row
)组成,它们的关系如下图:
表空间(tablespace)
在MySQL中,所有InnoDB存储引擎表中的数据都存储在表空间中。如果用户启用了innodb_file_per_table
,那么每张表内的数据可以存储在一个单独的表空间文件(称为独立表空间文件)中,如果没有启用,那么数据都会存储在共享表空间文件中(默认情况下的ibdata0
和ibdata1
文件)。在MySQL 5.7中,innodb_file_per_table
默认是启用的:
mysql> show variables like 'innodb_file_per_table';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_file_per_table | ON |
+-----------------------+-------+
1 row in set (0.05 sec)
独立表空间文件存放的只是这张表的数据、索引和插入缓冲Bitmap页。对于回滚信息、插入缓冲索引页、事务信息、二次写缓冲依然存放于共享表空间文件。
段(segment)
表空间由各个段组成,常见的段类型有:数据段、索引段、回滚段。
由于InnoDB表采用的是聚簇索引,所以数据段可以看成是B+树的叶子节点,索引段可以看成是B+树的非索引节点。
区(extend)
一个段由多个区组成,区由多个连续页组成,每个区的大小为1MB,默认情况下,每个页的大小为16KB:
mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.05 sec)
即一个区中一共有64个连续页。用户可通过innodb_page_size
参数设置每个页的大小。
默认情况下,用户在创建一张InnoDB表后,该表对应的独立表空间文件为96KB,在每个段开始时会先用32个碎片页来存放数据,使用完这32个页后才是64个连续页的申请。这么做是考虑到有些表的数据相对来说是比较少的,可以节省磁盘空间,因为申请64个页(即1个区)需要1MB空间。
页(page)
页是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
)
行(Row)
InnoDB存储引擎将数据按行进行存放,每个页最多存放7992行记录(16KB除以2~200),InnoDB存储引擎提供了Compact
、Redundant
、Compressed
、Dynamic
四种格式来存放行记录数据,用户可通过命令show table status like 'table_name'
来查看该属性:
mysql> show table status like 'emp';
+------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
| Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
| emp | InnoDB | 10 | Dynamic | 6 | 2730 | 16384 | 0 | 0 | 0 | NULL | 2019-08-10 21:07:05 | NULL | NULL | utf8mb4_general_ci | NULL | | |
+------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+--------------------+----------+----------------+---------+
1 row in set (0.08 sec)
其中Row_format
就是行记录格式。下面我们来深入探讨InnoDB的行记录格式是如何实现的。
行记录格式
Compact
Compact
行记录格式是在MySQL 5.0引入的,设计目标是高效地存储数据,每行数据的存储方式如下:
-
变长字段长度列表
Compact
行记录格式首部是一个非NULL的变长字段长度列表,按照列的顺序逆序放置,若列的长度小于255字节,则用1字节表示,若大于255个字节,用2字节表示。变长字段长度列表最大只能为2字节,因此VARCHAR
类型理论最大长度限制为65535字节)。变长字段长度列表的实际占用大小由变长列的数量及其属性决定。 -
NULL标志位(1字节)
NULL标志位记录了该行数据是否有NULL值,有则用1表示,该标志位占用1个字节 -
记录头信息(5字节)
名称 大小(位) 描述 未知 2 未知 deleted_flag` 1 该行是否被删除 min_rec_flag` 1 该行是否是预先被定位为最小的记录 n_owned` 4 该记录拥有的记录数 heap_no` 13 索引堆中该条记录的排序记录 record_type` 3 记录类型,000为普通,001为B+树节点指针,010为Infimum,011为Supremum,其它为保留用途 next_record` 16 页中下一条记录的相对位置,InnoDB通过该字段将各个行数据通过链表的方式串联在一起 -
列数据
最后的部分就是存储每个列的数据,需要注意的是NULL不占用任何空间,即NULL除了占有NULL标志位以外实际存储不占有任何空间。此外,除了实际存储数据的列以外,还存在两个隐藏列:事务ID列(6字节)、回滚指针列(7字节)。如果没有定义主键,每行还会自动添加一个6字节的rowid
列。
行溢出的处理
对于TEXT
、BLOB
或者长度比较大的VARCHAR
这种存储的数据量较大的列,因为超出了页的大小(16KB),也就是发生了行溢出。
在Compact
/Redundant
两种行记录格式下,InnoDB会将溢出的数据存储在Uncompress BLOB
类型的页中,没有溢出的数据部分依旧保存在数据页(B-Tree Node
)中。
B-tree Node
负责保存一行数据前768字节,之后记录了一个偏移量,指向行溢出页。
因为InnoDB是采用B+树进行索引组织的,所以每个页中至少应当有两行记录(不然每个页存储一行数据就是链表结构了),因此如果页中只能存放一条记录,那么InnoDB会将行数据存放到溢出页中。
Dynamic / Compressed
Dynamic / Compressed文件格式统称为Barracuda文件格式,这两种格式采用了完全行溢出的处理方式:数据页中只存储20个字节的指针,实际数据存放在Off Page
中:
Compressed
行记录还会使用zlib
算法对大长度数据进行压缩,能够节省存储空间。
数据页(B-tree Node)结构
InnoDB数据页由7个部分组成
File Header
(文件头),占用38字节Page Header
(页头),占用56字节Infimum
/Supremum Records
User Records
(用户记录 / 行记录)Free Space
(空闲空间)Page Directory
(页目录)File Trailer
(文件结尾信息),占用8字节
File Header
、Page Header
、File Trailer
大小是固定的,用来标记该页的的一些信息。User Records
、Free Space
、Page Directory
为实际的行记录存储空间,大小是动态的。
File Header组成结构
File Header
负责记录页的头信息,占用38字节:
FIL_PAGE_SPACE_OR_CHECKSUM
(4字节):页的checksum
的值FIL_PAGE_OFFSET
(4字节):页的偏移值,即该页在表空间的位置FIL_PAGE_PREV
(4字节):当前页的上一个页(B+树的叶子节点通过双向列表连接在一起)FIL_PAGE_NEXT
(4字节):当前页的下一个页FIL_PAGE_LSN
(8字节):代表该页最后被修改的日志序列位置FIL_PAGE_TYPE
(2字节):页的类型。当值为0x45BF
代表当前页是数据页,0x000A
表示是BLOB页,0x0003
表示是索引节点…FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID
(4字节):当前页属于哪个表空间
Page Header组成结构
PAGE_N_DIR_SLOTS
(2字节):在页目录(Page Directory
)中的槽(Slot
)数。PAGE_HEAP_TOP
(2字节):堆中第一个记录的指针PAGE_N_HEAP
(2字节):堆中记录数,第15位表示行记录格式PAGE_FREE
(2字节):指向可重用空间的首指针PAGE_GARBAGE
(2字节):已删除记录的字节数PAGE_LAST_INSERT
(2字节):最后插入记录的位置PAGE_DIRECTION
(2字节):最后插入的方向PAGE_N_DIRECTION
(2字节):一个方向连续插入记录的数量PAGE_N_RECS
(2字节):该页中记录的数量PAGE_MAX_TRX_ID
(8字节):修改当前页的最大事务IDPAGE_LEVEL
(2字节):当前页在索引树(B+ tree)位置PAGE_INDEX_ID
(8字节):索引ID,表示当前页属于哪一个索引PAGE_BTR_SEG_LEAF
(10字节):B+树数据页非叶节点所在段的segment header
PAGE_BTR_SEG_TOP
(10字节):B+树数据页所在段的segment header
Infimum / Supremum Record
在InnoDB引擎中每个页都有两个虚拟行记录用来限定记录的边界,Infimum
记录了比该页任何主键值都要小的值,Supremum Record
记录了比任何可能大的主键值还要大的值,这两个字段在页创建时被创建并且不会被删除,占用大小和行记录格式相关。Infimum
在行记录首部,Supremum Record
在行记录尾部。
User Record / Free Space
User Record
实际存储行记录的内容
Free Space
即空闲空间,也是采用“链表”的方式存储,当前页一条记录被删除后,该空间就会被加入到空闲链表中。
Page Directory
Page Directory
(页目录)存放了记录的相对位置(不是偏移量),页目录里面的这些记录指针称之为Slots
(槽)。对于InnoDB来说,并不是每个记录都拥有一个唯一的槽,因为每个槽里面可能包含了多条记录。Slots
中的记录按照索引键值存放,这样可以利用二叉查找快速找到记录的指针。
需要注意的是,B+树本身并不能找到具体的一条记录,只能找到该记录所在的页。数据库通过磁盘IO把页读入到内存,然后再通过页目录进行二叉查找,二叉查找在内存中的速度很快,一般会忽略这部分的时间。
File Trailer
为了检测页是否完整写入了磁盘,InnoDB存储引擎的页中设置了File Trailer
部分。
File Trailer
占用8个字节,前4个字节表示该页的checksum
值,最后4个字节和FileHeader
中的FIL_PAGE_LSN
相同。通过与这个值进行比较,来保证当前页是完整的。默认情况下InnoDB每从磁盘中读取页就会检验这个页的完整性。
参考资料
《MySQL技术内幕(InnoDB存储引擎)》