我们平时都是以记录为单位向表中插入数据的,这些记录在磁盘上的存放形式也被称为行格式或者记录格式。目前已经存在的行格式有 COMPACT、REDUNDANT、DYNAMIC 和 COMPRESSED。
COMPACT 行格式
一条完整的记录其实可以被分为记录的额外信息和记录的真实数据两大部分。
记录的额外信息
这部分信息是服务器为了更好地管理记录而不得不额外添加的一些信息。这些额外信息分为 3 个部分,分别是变长字段长度列表、NULL 值列表和记录头信息。
变长字段长度列表
MySQL支持一些变长的数据类型,比如 VARCHAR(M)、VARBINARY(M)、各种 TEXT 类型、各种 BLOB 类型。我们也可以把拥有这些数据类型的列称为变长字段。变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来,变长字段占用的存储空间分为两部分:
- 真正的数据内容
- 该数据占用的字节数
在 COMPACT 行格式中,所有变长字段的真实数据占用的字节数都存放在记录的开头位置,从而形成一个变长字段长度列表,各变长字段的真实数据占用的字节数按照列的顺序逆序存放!逆序存放!逆序存放!
如果该变长字段允许存储的最大字节数超过 255 字节,并且真实数据占用的字节数超过 127 字节,则使用 2 字节来表示真实数据占用的字节数,否则使用 1 字节。具体规则参考《MySQL是怎样运行的》4.3.2 COMPACT 行格式
变长字段长度列表中只存储值为非 NULL 的列的内容长度,不存储值为 NULL 的列的内容长度。
如果表中所有的列都不是变长的数据类型或者所有列的值都是 NULL 的话,就不需要有变长字段长度列表。
NULL 值列表
COMPACT 行格式把一条记录中值为 NULL 的列统一管理起来,存储到 NULL 值列表中。
- 首先统计表中允许存储 NULL 的列有哪些。
主键列以及使用 NOTNULL 修饰的列都是不可以存储 NULL 值的,所以在统计的时候不会把这些列算进去。 - 如果表中没有允许存储 NULL 的列,则 NULL 值列表也就不存在了,否则将每个允许存储 NULL 的列对应一个二进制位,二进制位按照列的顺序逆序排列,逆序排列,逆序排列。二进制位表示的意义如下:
- 二进制位的值为 1 时,代表该列的值为 NULL;
- 二迸制位的值为 0 时,代表该列的值不为 NULL。
- MySQL 规定 NULL 值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补 0。
记录头信息
记录头信息由固定的 5 字节组成,用于描述记录的一些属性。
这些属性代表的详细信息如下表所示:
另外,记录头信息的前4个位也被称为 info bit。
- deleted_flag:这个属性用来标记当前记录是否被删除,占用 1 比特。值为 0 时表示记录没有被删除,值为 1 时表示记录被删除了。这些被删除的记录之所以不从磁盘上移除,是因为在移除它们之后,还需要在磁盘上重新排列其他的记录,这会带来性能消耗,所以只打一个删除标记就可以避免这个问题。所有被删除掉的记录会组成一个垃圾链表,记录在这个链表中占用的空间称为可重用空间。之后若有新记录插入到表中,它们就可能覆盖掉被删除的这些记录占用的存储空间。
- heap_no:我们向表中插入的记录从本质上来说都是放到数据页的 User Records 部分,这些记录一条一条地亲密无间地排列着。InnoDB 把这种结构称之为堆(heap)。一条记录(这条记录的 deleted_flag 可以为 1)在堆中的相对位置称之为 heap_no。在页面前边的记录 heap_no 相对较小,在页面后边的记录 heap_no 相对较大,每新申请一条记录的存储空间时,该条记录比物理位置在它前边的那条记录的heap_no 值大 1。Infimum 记录的 heap_no 为 0,Supremum 记录的 heap_no 为 1。无论我们向页中插入了多少条记录,InnoDB 规定,任何用户记录都比 Infimum 记录大,任何用户记录都比 Supremum 记录小。Infimum 和 Supremum 这两条记录的构造十分简单,都是由 5 字节大小的记录头信息和 8 字节大小的一个固定单词组成的,如下图所示:
堆中记录的 heap_no 值在分配之后就不会发生改动了,即使之后删除了堆中的某条记录,这条被删除记录的 heap_no 值也仍然保持不变。 - next_record:它表示从当前记录的真实数据到下一条记录的真实数据的距离(字节数)。如果该属性值为正数,说明当前记录的下一条记录在当前记录的后面;如果该属性值为负数,说明当前记录的下一条记录在当前记录的前面。下一条记录指的并不是插入顺序中的下一条记录,而是按照主键值由小到大的顺序排列的下一条记录。而且规定 Infimum 记录的下一条记录就是本页中主键值最小的用户记录,本页中主键值最大的用户记录的下一条记录就是 Supremum 记录。无论怎么对页中的记录进行增删改操作 InnoDB 始终会维护记录的一个单向链表,链表中的各个节点是按照主键值由小到大的顺序链接起来的。当数据页中存在多条被删除的记录时,可以使用这些记录的 next_record 属性将这些被删除的记录组成一个垃圾链表,以备之后重用这部分存储空间。
记录的真实数据
记录中除了我们自己定义的列的数据外,MySQL 会为每个记录默认地添加一些列(也称为隐藏列),具体如下所示:
实际上这几个列的真正名称是 DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,表格中为了易于阅读才改成小写。
InnoDB 表的主键生成策略:优先使用用户自定义的主键作为主键,如果用户没有定义主键,则选取一个不允许存储 NULL 值的 UNIQUE 键作为主键;如果表中连不允许存储 NULL 值的 UNIQUE 键都没有定义,则 InnoDB 会为表默认添加一个名为 row_id 的隐藏列作为主键。