InnoDB存储引擎笔记-表

Scroll Down

概述

本章将从InnoDB存储引擎表的逻辑存储及实现开始进行介绍,然后将重点分析表的物理存储特征,即数据在表中是如何组织和存放的。简单来说,表就是关于特定实体的数据集合,这也是关系型数据库模型的核心。

索引组织表

在InnoDB存储引擎中,表都是根据主键顺序组织存放的,这种存储方式的表称为索引组织表(index organized table)。在InnoDB存储引擎表中,每张表都有个主键(Primary Key),如果在创建表时没有显示地定义主键,则InnoDB存储引擎会按如下方式选择或创建主键:

  • 首先判断表中是否有非空的唯一索引(Unique NOT NULL),如果有,则该列即为主键。
  • 如果不符合上述条件,InnoDB存储引擎会自动创建一个6字节大小的指针。

当表中有多个非空唯一索引时,InnoDB存储引擎将选择建表时第一个定义的非空唯一索引为主键。这里需要非常注意的是,主键的选择根据的是定义索引的顺序,而不是建表时列的顺序。

InnoDB逻辑存储结构

从InnoDB存储引擎的逻辑存储结果看,所有数据都被逻辑地存放在一个空间中,称为表空间(tablespace)。表空间又由段(segment)、区(extent)、页(page)组成。页在一些文档中被称为块(block)。
innodb-note-table01.drawio

表空间

表空间可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。在默认情况下InnoDB存储引擎有一个共享表空间ibdata1,即所有数据都存放在这个表空间内。如果用户启用了参数innodb_file_per_table,则每张表内的数据可以单独放到一个表空间内。
如果启用了innodb_file_per_table的参数,需要注意的是每张表的表空间内存放的只是数据、索引和插入缓冲Bitmap页,其他类的数据、如回滚(undo)信息,插入缓冲索引页、系统事务信息,二次写缓冲(Double write buffer)等还是存放在原来的共享表空间内。

表空间是由各个段组成的,段一般分为数据段、索引段和回滚段等。我们知道,InnoDB 默认是基于 B + 树实现的数据存储。
这里的索引段则是指的 B + 树的非叶子节点,而数据段则是 B + 树的叶子节点。而回滚段则指的是回滚数据,之前我们在讲事务隔离的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。
上图显示了表空间是由各个段组成的,常见的段有数据段、索引段、回滚段等。因为前面介绍过InnoDB存储引擎表是索引组织的(index organized)、因此数据即索引,索引即数据。那么数据段即为B+树的叶子节点(Leaf node segement),索引段即为B+树的非索引节点(Non-leaf node segment)。在InnoDB存储引擎中,对段的管理都是由引擎自身所完成。

区是由连续的页组成的空间,在任何情况下每个区的大小都为1MB。为了保证区中页的连续性,InnoDB存储引擎一次从磁盘申请4-5个区。在默认的情况下,InnoDB存储引擎的大小为16KB,即一个区中一共有64个连续页。InnoDB1.0.x版本开始引入压缩页,即每个页的大小可以通过参数KEY_BLOCK_SIZE设置为2K、4K、8K,因此每个区对应页的数量就应该为512、256、128。
InnoDB1.2.x版本新增了参数innodb_page_size,通过该参数可以将默认页的大小设置为4K,8K,但是页中数据库不是压缩。这时区中页的数量同样也为256,128。总之,不论页的大小怎么变化,区的大小总是为1M。
但是,这里还有这样一个问题:在用户启用了参数innodb_file_per_table后,创建的表默认大小是96KB。区中是64个连续的页,创建的表的大小至少是1MB才对啊?其实这是因为在每个段开始时,先用32个页大小的碎片页(fragment page)来存放数据,在使用完这些页之后才是64个连续页的申请。这样做的目的是,对于一些小表,或者是undo这类的段,可以在开始时申请较少的空间,节省磁盘容量的开销。

同大多数数据库一样,InnoDB有页(page)的概念(也可以称为块)。页是InnoDB磁盘管理的最小单位。在InnoDB存储引擎中,默认每个页的大小为16KB。而从InnoDB1.2.x版本开始,可以通过innodb_page_size将页的大小设置为4K,8K,16K。若设置完成,则所有表中页的大小都为innodb_page_size,不可以对其再次进行修改。除非通过mysqldump导入和导出操作来产生新的库。
在InnoDB存储引擎中,常见的页类型有:

  • 数据页(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行记录。

InnoDB行记录格式

InnoDB存储引擎和大多数数据库一样,记录是以行的形式存储的。这意味着页中保存着表中一行行的数据。在InnoDB1.0.x版本之前,InnoDB存储引擎提供了Compact和Redundant两种格式来存放行记录数据,这也是目前使用最多的一种格式。在MySQL5.1版本中,默认设置为Compact行格式。用户可以通过命令SHOW TABLE STATUS LIKE ‘table_name’来查看当前表使用的行格式,其中row_format属性表示当前所使用的行记录结构类型。

Compact行记录格式

Compact行记录是在MySQL5.0中引入的,其设计的目标是高效地存储数据。简单来说,一个页中存放的行数据越多,其性能就越高。
COMPACT.png
从上图可以观察到,Compact行记录格式的首部是一个非NULL变长度列表,并且其是按照列的顺序逆序设置的,其长度为:

  • 若列的长度小于255字节,用1字节表示。
  • 若大于255个字节,用2字节表示。

变长字段的长度最大不可以超过2字节,这是因在MySQL数据库中VARCHAR类型的最大长度限制为65535。变长字段之后的第二个部分是NULL标志位,该位指示了该行数据中是否有NULL值,有则用1表示。该部分所占的字节应该为1字节。接下来的部分是记录头信息(record header),固定占用5字节(40位)。
最后的部分就是实际存储的每个列的数据,需要特别注意的是,NULL不占该部分任何空间,即NULL除了占有NULL标志位,实际存储不占有任何空间。另外有一点需要注意的是,每行数据处理用户定义的列外,还有两个隐藏列,事务ID列和回滚指针列,分别为6字节和7字节大小。若InnoDB表没有定义主键,每行还会增加一个6字节的rowid列。

行溢出数据

InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。一般认为BLOB、LOB这类的大对象列类型的存储会把数据存放在数据页之外。但是,这个理解有点偏差,BLOB可以可以不将数据放在溢出页面,而且即便是VARCHAR列数据类型,依然有可能被存放为行溢出数据。

InnoDB数据页结构

页是InnoDB存储引擎管理数据库的最小磁盘单位。页类型为B-tree Node的页存放的即是表中行的实际数据了。
InnoDB数据页由以下7个部分组成。

  • File Header(文件头)
  • Page Header(页头)
  • Infimum和Supremum Records
  • User Records(用户记录,即行记录)
  • Free Space(空闲空间)
  • Page Directory(页目录)
  • File Trailer(文件结尾信息)

其中File Header、Page Header、File Trailer的大小是固定的,分别为38,56,8字节,这些空间用来标记该页的一些信息。User Records、Free Space、Page Directory这些部分为实际的行记录存储空间,因此大小是动态的。
mysql-data-page.drawio

File Header

File Header用来记录页的一些头信息。共占用38字节

名称 大小(字节) 说明
FIl_PAGE_SPACE_OR_CHKSUM 4 当MySQL为MySQL4.0.14之前的版本时,该值为0.在之后的MySQL版本中,该值代表页的checksum值
FIL_PAGE_OFFSET 4 表空间中页的偏移值。如果独立表空间a.ibd的大小为1GB,如果页的大小为16KB,那么总共有65536个页、FIL_PAGE_OFFSET表示该页所在所有页中的位置。若此表空间的ID为10,那么搜索页(10,1)就表示查找表a中的第二个页
FIL_PAGE_PREV 4 当前页的上一个页,B+Tree特性决定了叶子节点必须是双向链表
FIL_PAGE_NEXT 4 当前页的下一个页,B+Tree特性决定了叶子节点必须是双向链表
FIL_PAGE_LSN 8 该值代表该页最后被修改的日志序列位置LSN(Log Sequence Number)
FIL_PAGE_TYPE 2 InnoDB存储引擎页的类型。常见下表
FIL_PAGE_FILE_FLUSH_LSN 8 该值仅在系统表空间的一个页中定义,代表文件至少被更新到了该LSN值。对于独立表空间,该值都为0
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 从MySQL4.1开始,该值代表页属于哪个表空间
InnoDB存储引擎中页的类型
名称 十六进制 解释
FIL_PAGE_INDEX 0x45BF B+树叶节点
FIL_PAGE_UNDO_LOG 0x0002 Undo log页
FIL_PAGE_INODE 0x0003 索引节点
FIL_PAGE_IBUF_FREE_LIST 0x0004 Insert Buffer空闲列表
FIL_PAGE_TYPE_ALLOCATED 0x0000 该页为最新分配
FIL_PAGE_IBUF_BITMAP 0x0005 Insert Buffer位图
FIL_PAGE_TYPE_SYS 0x0006 系统页
FIL_PAGE_TYPE_TRX_SYS 0x0007 事务系统数据
FIL_PAGE_TYPE_FSP_HDR 0x0008 File Space Header
FIL_PAGE_TYPE_XDES 0x0009 扩展描述页
FIL_PAGE_TYPE_BLOB 0x000A BLOB页

接着File Header部分的是Page Header,该部分用来记录数据页的状态信息,由14个部分组成,共占用56字节。

Page Header组成部分
名称 大小(字节) 说明
PAGE_N_DIR_SLOTS 2 在Page Directory(页目录)中的Slot槽数
PAGE_HEAP_TOP 2 堆中第一个记录的指针,记录在页中是根据堆的形式存放的
PAGE_N_HEAP 2 堆中的记录数,一共占用2字节
PAGE_FREE 2 指向可重用空间的首指针
PAGE_GARBAGE 2 已删除记录的字节数,即行记录结构中delete_flag为1的记录大小的总数
PAGE_LAST_INSERT 2 最后插入记录的位置
PAGE_DIRECTION 2 最后插入的方向
PAGE_N_DIRECTION 2 一个方向连续插入记录的数量
PAGE_N_RECS 2 该页中记录的数量
PAGE_MAX_TRX_ID 8 修改当前页的最大事务ID,注意该值仅在Secondary Index中定义
PAGE_LEVEL 2 当前页在索引树中的位置,0x00代表叶节点,即叶节点总是在第0层
PAGE_INDEX_ID 8 索引ID,表示当前页属于哪个索引
PAGE_BTR_SEG_LEAF 10 B+树数据页非叶节点所在段的segment header,注意该值仅在B+树的Root页中定义
PAGE_BTR_SEG_TOP 10 B+树数据页所在段的segment header,注意该值仅在B+树中的Root页中定义

Infimum和Supremum Record

在InnoDB存储引擎中,每个数据页中有两个虚拟的行记录,用来限定记录的边界。Infimum记录是比该页中任何主键值都要小的值,Supremum指比任何可能大的值还要大的值。这两个值在页创建时被建立,并且在任何情况下不会被删除。在Compact行格式和Redunant行格式下,两者占用的字节数各不相同。
innodb-note-infimum.drawio

User Record和Free Space

User Record就是之前讨论过的部分,即实际存储行记录的内容。再次强调,InnoDB存储引擎表总是B+树索引组织的。
Free Space很明显指的就是空闲空间,同样也是个链表数据结构。在一条记录被删除后,该空间会被加入到空间链表中。

Page Directory

Page Directory(页目录)中存放了记录的相对位置(注意,这里存放的是页相对位置,而不是偏移量),有些时候这些记录指针称为Slots(槽)或目录槽(Directory Slots)。与其他数据库系统不同的是,在InnoDB中并不是每个记录拥有一个槽,InnoDB存储引擎的槽是一个稀疏目录(sparse directory),即在一个槽中可能包含多个记录。伪记录Infimum的n_owned值总是1,记录Supremum的n_owned的取值范围为[1,8],其他用户记录n_owned的取值范围为[4,8]。当记录被插入或删除时需要对槽进行分裂和平衡的维护操作。
在Slots中记录按照索引键值顺序存放,这样可以利用二叉树查找迅速找到记录的指针。由于在InnoDB存储引擎中Page Directory是稀疏目录,二叉查找的结果只是一个粗略的结果,因此InnoDB存储引擎必须通过recorder header中的next_record来继续查找相关记录。同时,Page DIrectory很好地解释了recorder header中的n_owned值的含义,因为这些记录并不包含在Page Directory中。
需要牢记的是,B+树索引本身并不能查找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存中,然后通过Page Directory再进行二叉查找。只不过二叉查找的时间复杂度很低,同时在内存中的查找很快,因此通常忽略这部分查找所用的时间。

File Trailer

为了检测页是否已经完整地写入磁盘(如可能发生的写入过程中磁盘损坏、机器关机等)。InnoDB存储引擎的页中设置了File Trailer部分。
File Trailer只有一个FIL_PAGE_END_LSN部分,占用8字节。前4字节代表该页的checksum值,最后4字节的File Header中的FIL_PAGE_SPACE_OR_CHKSUM和FIL_PAGE_LSN值进行比较,看是否一直(checksum的比较需要通过InnoDB的checksum函数来进行比较,不是简单的等值比较),以此来保证页的完整性。
在默认配置下,InnoDB存储引擎每次从磁盘读取一个页就会检测该页的完整性,即页是否发生Corrupt,这就是通过File Trailer部分进行检测,而该部分的检测会有一定的开销。用户可以通过参数innodb_checksums来开启或关闭对这个页完整性的检查。
MySQL5.6.6版本开始新增了参数innodb_checksum_algorithm,该参数用来控制检测checksum函数的算法,默认的值有:innodb、crc32、none、strict_innodb、strict_crc32、strict_none。

约束

约束和索引的区别

Primary key和Unique Key约束。约束和索引的区别,当用户创建了一个唯一索引就创建了一个唯一的约束。但是约束和索引的概念还是有所不同的,约束更是一个逻辑概念,用来保证数据的完整性,而索引是一个数据结构,既有逻辑上的概念,在数据库中还代表着物理存储的方式。