MySQL深入学习(3)——基于InnoDB的数据库表结构

1. InnoDB逻辑存储结构

在InnoDB存储引擎的逻辑存储结构中,每一个表下的所有数据都会被放在同一个空间中,这个空间又被称为表空间(tablespace)。往下细分,表空间又由段(segment)组成,段由区(extent)组成,区由页(page,或者被称为块,block)组成,如下图所示

1.1 表空间

    表空间可以看做是InnoDB存储引擎逻辑结构的最高层,表的所有相关数据都会放在表空间中。默认情况下,InnoDB有一个共享表空间ibdatal,即所有的的数据都会存放在该表空间中,但是如果开启了配置参数innodb_file_per_table,那么每张表内的数据会单独的放在一个表空间内。

    但是,即使开启了innodb_file_per_table配置,也只是将每张表的数据、索引和插入缓冲bitmap页放在了单独的表空间而已,而对于其他数据(比如回滚信息、插入缓冲索引页、系统事务信息、二次写缓冲等)都还是存放在共享表空间中。所以,即使开启了innodb_file_per_table配置,共享表空间的大小仍然会不断增长。

1.2 段

    表空间是由段组成的,常见的段有数据段、索引段、回滚段等。数据段就是B+树的叶子节点,索引段就是B+树的非叶子节点。而一个段由多个区组成,但并没有对区的数量限制。

1.3 区

    区是有连续的页组成的空间,在任何情况下每个区的大小都为1mb,为了保证区中页的连续性,innoDB存储引擎每次都会从磁盘中申请4到5个区大小的完整连续空间,在默认情况下,innoDB存储引擎的页的大小为16kb,即一个区中会有64个连续的页。

    在InnoDB1.0版本开始引入压缩页,即每个页的大小可以通过参数key_block_size设置为2kb、4kb或8kb,那么每个区对应辉持有连续页的数量就是512、256、128。总之,无论页的大小如何变化,区的大小始终是1mb。

1.4 页

    页是Innodb磁盘文件管理的最小单位(注意,这里要和操作系统磁盘操作的最小单位区分开,大部分操作系统一次读取的最小单位基本都是4kb,可以去了解一下操作系统中的文件管理,扇区、簇、页,其实与数据库系统很相似的,所以对应的数据库系统的磁盘文件管理的最小单位基本都是操作系统文件管理最小单位的倍数)。在InnoDB存储引擎中,默认每个页的大小为16kb。从InnoDB1.2以后,可以通过参数innodb_page_size设置页的大小为4k、8k或16k。若设置完成,则飙中所有页的大小都为设置的数值,而且不可以再次修改。

    在Innodb中,常见的页类型有:

(1)数据页(B-tree Node)。

(2)Undo页(Undo Log Page)。

(3)系统页(System Page)。

(4)事务数据页(Transaction system Page)。

(5)插入缓冲位图页(Insert Buffer Bitmap)。

(6)插入缓冲空闲列表页(Insert Buffer Free List)。

(7)未压缩的二进制大对象页(Uncompressed BLOB Page)。

(8)压缩的二进制大对象页(Compressed BLOB Page)。

1.5 行

    innodb存储引擎是面向行的,也就是说数据是按照行进行存放的。每个页存放的行记录也是有数量限制的,最多允许存放(16kb/2)-200,也就是7992行数据。

 

2. innodb行记录的格式

    InnoDB存储引擎是面向行存储数据的,这意味着在数据页中保存着表中一行行的数据。在InnoDB中,一共有两种格式来存放行记录,分别是Compact和Redundant,Redundant是为了兼容老版本的数据格式而保留的,MySQL5.1之后基本都是默认为Compact格式或者是基于Compact的格式(Dynamic)来存储行记录。

2.1 Compact行记录

    Compact行记录的出现提高了MySQL的存储性能,简单来说,一个页中存储的行数据越多,那么其性能就越高,原因是为什么,这个涉及到索引以及B+树的问题(后续在索引中具体进行讲解),简单来说就是一个表的行记录格式决定表的行物理存储模式,决定query和dml操作性能,越多的行匹配进独立的磁盘页,query和index查找会快一些,需要的缓存及io操作就会少一些(专业术语来说,就是在其他博文中常见到的扇入扇出)。存储结构如下图所示

compact

(1)依据上图中我们可以看到,Compact行记录格式的首部是一个非Null变长字段长度列表,并且是按照列的顺序逆序放置的,若列(字段)的长度小于255字节,用1字节表示。若列的长度大于255字节,用2字节表示。变长字段的字节长度最大不可以超过2字节,这是因为MySQL数据库中Varchar类型的数据最大长度限制为65535字节。

(2)变长字段之后的第二个部分是NULL标志位,该位表示了该行数据中是否有NULL值,有则用1表示,占一个字节。

(3)记录头信息:固定占用5字节,每位的含义见下表

名称 大小 (bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型,0 表示普通记录,1 表示B+树非叶子节点记录,2 表示最小记录,3 表示最大记录
next_record 16 表示下一条记录的相对位置

(4)后面的就是实际存储的每个列的数据,在每个列的存储数据中,NULL不占该部分任何空间。此外还有两个隐藏列,事务ID列和回滚指针列,分别为6字节和7字节。若innodb表没有定义主键,每行还会增加一个6字节的rowid列。

2.2 Redundant行记录格式

    redundant是MySQL5.0之前的InnoDB的行记录存储方式,MySQL5.0支持Redundant是为了兼容之前版本的页格式,其行记录结构如下图所示

Redundant行记录格式的首部是一个字段长度偏移列表,同样是按照列的顺序逆序放置的。同样的,若表中各列的长度之和小于255字节,用1字节表示;若大于255字节,用2字节表示。

第二个部分是记录头信息,占用6字节,每个比特位的含义如下所示。其中n_fields值代表的就是一行中列的数量,占用10位,这也是为什么mysql中一张表的字段数量最多为1023个。

后续的数据就是每个列的具体数据了。

名称 大小 (bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在页面堆的位置信息
n_field 10 表示记录中列的数量
1byte_offs_flag 1 标记字段长度偏移列表中每个列对应的偏移量是使用1字节还是2字节表示的
next_record 16 表示下一条记录的相对位置

2.3 行溢出数据

    InnoDB会将一条记录中的某些数据存储在真正的数据页之外,比如BLOB、TEXT这类大对象列类型的存储会把数据存放在数据页面之外。但是,这个说法其实不完全正确,BLOB也有可能会把数据放在溢出页面,而且即便是VARCHAR类型的数据列,也有可能被存放在行溢出数据。

    首先可以看看为什么VARCHAR可能会被存储在行溢出数据之中。首先,在MySQL中定义VARCHAR类型的字段时,VARCHAR字段的限制空间大小为65535字节,但是通过实验发现,其实并不能支持定义一个65535字节长度的字段,如下图所示

导致报错的原因有多个:

(1)如果将SQL_MODE设置为严格模式,建表语句中如果有65535字节长度的的VARCHAR,肯定会报错的,因为肯定还会有一些其他开销,实际上最多存储65532字节的数据。如果没有设置为严格模式,那么数据库会自动的将该列的数据类型转为TEXT类型,这样就不会报错了。

(2)VARCHAR后面跟的限制数量,实际上并不是指的字节数量,而是指字符数量,那么这就导致了一个问题:如果字符编码使用的是Latin1字符集,那么这么说是没错的,因为Latin1字符集中1个字符占用1个字节;而如果是采用UTF-8字符集,那么就是有问题的了,因为UTF-8是采用了3个字节空间来表示一个字符(所以最大限制为21845),而GBK用2个字节空间来表示字符(最大32767),所以在不同的字符集编码规则下,VARCHAR的长度限制也是不同的。

(3)最重要的一点,这个65535实际上并不是指某一列的字节长度限制,而是指所有VARCHAR列的长度之和要小于65535字节。也就是说,如果你定义了三个VARCHAR列,每个列的限制长度为2400,那么同样也会报错,无法建表。

搞清楚VARCHAR列的65535字节限制到底是限制了什么,再来看看另一个问题:mysql中一个数据页最大为16kb,也就是16384字节,那么为什么一个数据页能存储65532个字节的数据呢?这就涉及到InnoDB对于行溢出数据数据的处理方式。

    一般情况下,InnoDB引擎的数据都是存放在B tree Node类型的数据页中,但是当发生行溢出时,数据就会存放在UNcompress Blob类型的数据页中。而相应的在原数据页的行记录中(Compact 和 Reduntant两种格式 ),'记录的真实数据' 处只会存储一部分 (768 字节的) 数据,剩下的数据存储在几个其他的页 (溢出页) 中 (以链表的方式连接),在 '记录的真实数据' 处用 20 个字节存储这些页的地址 。如下图所示
在这里插入图片描述

    其实换句话说,如果数据页中一行记录的大小超过了页的大小,那么该行数据的完整数据一定会被放在uncompress blob类型的数据页中,但一定要注意,并不是说如果插入的一行数据大于页的空间,那么该页就会只存放这一行记录,原因就在于InnoDB是将数据按照B+树结构进行的存储,那么每个数据页中至少会存放两条行记录,如果一个数据页只存放一行数据,那么整个数据结构几乎就变成了链表,这样也就失去了B+树的意义(这一块就涉及到平衡二叉树为什么要优于普通二叉树)。所以,对于行溢出数据主体会存储在uncompress blob页中,这样就能保证每个数据页(B tree Node)中能够存储多条行记录。同样的,对于BLOB这种类型的数据,如果长度不足以发生行溢出的话,那么数据仍然还是会存储在数据页中,但如果发了行溢出,那么存储方式还是和上面所说的一样。

    InnoDB1.0版本之后又引入了两种新的行记录存储格式,分别是compressed和dynamic。这两种数据格式对于行溢出数据的处理则采用了不同的方式,这两种格式在发生行溢出的时候,数据页中只存放20个字节的指针,该指针会指向实际存放数据的off  page中,而且对于BLOB会完全采用行溢出处理,无论其长度是否会造成行溢出。而且对于Compressed的另一个功能就是,存储在其中的行数据会以zlib算法压缩数据,因此对于BLOB、TEXT这类大类型数据有效存储。

 

    通常我们都知道,VARCHAR是变长字符类型,而CHAR是定长字符类型。但必须要记住一点,这个定长和变长指的是字符数量,而不是字节数量。

    前面也说过了,在不同的编码字符集下,每个字符占用的空间大小是不同的,在Latin1编码格式中一个字符只有1字节,但是在UTF-8中一个字符就占了3字节。所以,一定要搞清楚这个定长定的是什么的数量。

 

3 InnoDB数据页的结构(大概了解即可)

    页是InnoDB存储引擎管理数据库的最小磁盘单位,页类型为B-tree Node的页存放的就是表中行记录的数据。一个页的大小一般都是16k,InnoDB每次从磁盘中将数据读取到内存中时,也都是整页的读取。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页

    其中数据页由以下几个部分组成:结构图如下

(1)File Header (文件头)

(2)Page Header(页头)

(3)Infimun和Supremum Records

(4)User Records(用户记录,即行记录)

(5)Free Space(空闲空间)

(6)Page Directory(页目录)

(7)File Trailer(文件结尾信息)

InnoDB的页结构分为七个部分,各个部分对应的作用如下面的表格所示:

名称 中文名 占用空间大小 简单描述
File Header 文件头 38字节 描述页的信息
Page Header 页头 56字节 页的状态信息
Infimum + SupreMum 最小记录和最大记录 26字节 两个虚拟的行记录(后面会说明)
User Records 用户记录 不确定 实际存储的行记录内容
Free Space 空闲空间 不确定 页中尚未使用的空间
Page Directory 页目录 不确定 页中的记录相对位置
File Trailer 文件结尾 8字节 结尾信息

3.1 File Header

    File Header用来记录页的一些头信息,由8个部分组成,共占用38字节,大小固定不变,如下表所示:

File header组成部分
名称 大小(字节) 说明
FIL_PAGE_SPACE_OR_CHKSUM 4 当 MySQL为 MySQL40.14之前的版本时,该值为0。在之后的 MySQL版本中,该值代表页的 checksum值(一种新的 checksum值)
FIL_PAGE_OFFSET 4 表空间中页的偏移值。如某独立表空间a.ibd的大小为1GB,如果页的FIL_PAGE_OFFSET大小为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存储引擎页的类型。常见的类型见下表。记住0x45BF,该值代表了存放的是数据页,即实际行记录的存储空间
LSN_FLUSH_LSN 8 该值仅在系统表空间的一个页中定义,代表文件至少被更新到了该LSN_FLUSH_LSN值。对于独立表空间,该值都为0
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 从 MySQL4.1开始,该值代表页属于哪个表空间

3.2 Page Header

    Page Header用来记录数据页的状态信息,由14个部分组成,共占用56个字节,大小固定不变。如下表所示

Page Header组成部分
名称 大小(字节) 说明
PAGE_N_DIR SLOTS 2 在 Page Directory(页目录)中的Slot(槽)数,“Page Directory”小节中会介绍
PAGE_HEAP_TOP 2 堆中第一个记录的指针,记录在页中是根据堆的形式存放的
PAGE_N_HEAP 2 堆中的记录数。一共占用2字节,但是第15位表示行记录格式
PAGE_FREE 2 指向可重用空间的首指针
PAGE_GARBAGE 2 已删除记录的字节数,即行记录结构中 delete flag为1的记录大小的总数
PAGE_LAST_INSERT 2 最后插入记录的位置
PAGE_DIRECTION 2 最后插入的方向。可能的取值为:
PAGE_LEFT(0x01)
PAGE_RIGHT(0x02)
PAGE_DIRECTION
PAGE_SAME_REO(0x03)
PAGE_SAME_PAGE(0x04)
PAGE_NO_DIRECTION (Ox05)
PAGE_N_DIRECTION 2 一个方向连续插人记录的数量
PAGE_NRECS 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页中定义

3.3 Infimum和Supremum Record

    在InnoDB存储引擎中,每个数据页中都有两个虚拟的行记录,用来限定记录的边界。Infimum行记录是比该页中任何主键值都要小的值,Supremum指比任何可能大的值还要大的值。这两个值在页创建的时候被建立,并且在任何情况下都不会被删除。

3.4 User Records和Free Space

    User Records就是指存储记录的地方和Free Space就是指页中的剩余空闲空间,我们在存储数据的时候,记录会存储到User Records部分 。但是在一个页新形成的时候是不存在User Records这个部分的,每当我们在插入一条记录的时候,都会从Free Space中去申请一块大小符合该记录大小的空间并划分到User Records,当Free Space的部分空间全部被User Records部分替换掉之后,就意味着当前页使用完毕,如果还有新的记录插入,需要再去申请新的页,过程如下:

3.5 Page Directory

    Page Directory(页目录)中存放了行记录的相对位置(存放的是在页中的相对位置,而不是偏移量),这些记录指针也被称为Slots(槽)或目录槽,在InnoDB中并不是每个记录拥有一个槽位,InnoDB存储引擎的槽位是一个稀疏目录,即一个槽位可能包含页中多个行记录的指针。怎么说呢,其实很类似于二分法查找(或者说二叉树查找),页中的所有行记录其实就是一个数组,而在page directory中存储的就是其中的几条比较标志性的行记录指针,比如说[a,b,c,d,e,f,g,h,i,l]是所有的行记录(相当于一个有序链表),那么page directory中就包含3条指针,分别指向行记录[a, e, l],当查询某一个具体行记录时,只需要于page directory中的指针行记录进行比较,就可以然后一个粗略的结果或者说区间范围,比如说查找行记录数据 g ,那么就会得到一个区间范围是e到l,然后从行记录e开始向后遍历进行查找就可以得到具体的目标行记录数据。

3.6 File Trailer

    为了检测页是否完整的写入了磁盘,InnoDB中设置了页的File Trailer部分,File Trailer只有一部分内容file_page_end_lsn,占用8字节。前4个字节代表该页的checksum值,最后的4个字节代表该页最后被修改的日志序列位置。将这两个值与File Header中的file_page_space_or_chksum和file_page_end_lsn两个值进行比较是否一致,以此判断页的完整性,当然不是简单的等值比较法,有一套特殊的比较算法。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章