MySQL是怎样运行的:(4)InnoDB记录结构
Session 4 InnoDB记录结构
InnoDB
将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
记录在磁盘上的存放方式也被称为行格式
或者记录格式
。常见的行格式有Compact
、Redundant
、Dynamic
和Compressed
。我们可以在创建或修改表的语句中指定行格式
:
1 | CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称 |
COMPACT行格式(紧凑行格式)
一条完整的记录其实可以被分为==记录的额外信息==和==记录的真实数据==两大部分。
(一)记录的额外信息
这部分信息是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3类,分别是变长字段长度列表
、NULL值列表
和记录头信息
变长字段长度列表
MySQL
支持一些变长的数据类型,比如VARCHAR(M)
、VARBINARY(M)
、各种TEXT
类型,各种BLOB
类型,我们也可以把拥有这些数据类型的列称为变长字段
。在Compact
行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表,各变长字段数据占用的字节数按照列的顺序逆序存放
1 | mysql> CREATE TABLE record_format_demo ( |
变长字段长度列表中只存储值为 非NULL 的列内容占用的长度,值为 NULL 的列的长度是不储存的 。
上述例子中,c1 c2的长度为0x04和0x03,又由于逆序存放,故变长字段长度列表为:
NULL值列表
如果把NULL
值都放到记录的真实数据
中存储会很占地方,所以Compact
行格式把这些值为NULL
的列统一管理起来,存储到NULL
值列表中。
如果表中没有允许存储 NULL 的列,则NULL值列表也不存在了,否则将每个允许存储NULL
的列对应一个二进制位,二进制位按照列的顺序逆序排列。二进制位的值为1
时,代表该列的值为NULL
。
MySQL
规定NULL值列表
必须用整数个字节的位表示,所以尽管例子中只有c1 c3 c3三个允许NULL的字段,但列表会拓展到8位(高位为0)。
此条记录的NULL值列表用十六进制表示就是:0x06。
记录头信息
除了变长字段长度列表
、NULL值列表
之外,还有一个用于描述记录的记录头信息
,它是由固定的5
个字节组成。5
个字节也就是40
个二进制位,不同的位代表不同的意思,如图:
名称 | 大小(单位: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 |
表示下一条记录的相对位置 |
这里暂不作展开。
(二)记录的真实数据
对于record_format_demo
表来说,记录的真实数据
除了c1
、c2
、c3
、c4
这几个我们自己定义的列的数据以外,MySQL
会为每个记录默认的添加一些列(也称为==隐藏列==),具体的列如下:(列名只是助理解,非真实)
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id |
否 | 6 字节 |
行ID,唯一标识一条记录 |
transaction_id |
是 | 6 字节 |
事务ID |
roll_pointer |
是 | 7 字节 |
回滚指针 |
这里需要提一下InnoDB
表对主键的生成策略:优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique
键作为主键,如果表中连Unique
键都没有定义的话,则InnoDB
会为表默认添加一个名为row_id
的隐藏列作为主键。
1 | mysql> CREATE TABLE record_format_demo ( c1 varchar(65535) ) charset=ascii row_format=COMPACT; |
为什么是65532?
MySQL
对一条记录占用的最大存储空间是有限制的,除了BLOB
或者TEXT
类型的列之外,其他所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535
个字节。
我们为了存储一个VARCHAR(M)
类型的列,其实需要占用3部分存储空间:
- 真实数据
- 真实数据占用字节的长度
NULL
值标识,如果该列有NOT NULL
属性则可以没有这部分存储空间
如果该VARCHAR
类型的列没有NOT NULL
属性,那最多只能存储65532
个字节的数据,因为真实数据的长度可能占用2个字节,NULL
值标识需要占用1个字节。
- 假设某个字符集中表示一个字符最多需要使用的字节数为
W
,比方说utf8
字符集中的W
就是3
,gbk
是2
,ascii
是1
。 - 对于变长类型
VARCHAR(M)
来说,这种类型表示能存储最多M
个字符(注意是M是字符不是字节),所以这个类型能表示的字符串最多占用的字节数就是M×W
。
如果M×W <= 255
,那么用1个字节来表示真正字符串占用的字节数。否则,假设它实际存储的字符串占用的字节数是L
:
- 如果
L <= 127
,则用1个字节来表示真正字符串占用的字节数。 - 如果
L > 127
,则用2个字节来表示真正字符串占用的字节数。
如果VARCHAR(M)
类型的列使用的不是ascii
字符集,那M
的最大取值取决于该字符集表示一个字符最多需要的字节数。在列的值允许为NULL
的情况下,gbk
字符集表示一个字符最多需要2
个字节,那在该字符集下,M
的最大取值就是32766
(也就是:65532/2),也就是说最多能存储32766
个字符;utf8
字符集表示一个字符最多需要3
个字节,那在该字符集下,M
的最大取值就是21844
,就是说最多能存储21844
(也就是:65532/3)个字符。
对于 char(M) 类型的列来说,当列采用的是定长字符集(比如ascii)时,该列占用的字节数不会被加到变长字段长度列表,而如果采用变长字符集时,该列占用的字节数也会被加到变长字段长度列表(但不影响char(M)本身定长,比如使用gbk
字符集的CHAR(10)
类型的列占用的真实数据空间始终为20
个字节)。
行溢出
对于Compact
和Reduntant
行格式来说,在Compact
和Reduntant
行格式中,对于占用存储空间非常大的列,在记录的真实数据
处只会存储该列的一部分数据,把剩余的数据分散存储在几个其他的页中,然后记录的真实数据
处用20个字节存储指向这些页的地址,从而可以找到剩余数据所在的页。
在本记录的真实数据处只会存储该列的前768
个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中,这个过程也叫做==行溢出==,存储超出768
字节的那些页面也被称为==溢出页==。
行溢出临界点:不用关注这个临界点是什么,只要知道如果我们向一个行中存储了很大的数据时,可能发生行溢出
的现象
Dynamic
行格式把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址;Compressed
行格式还会采用压缩算法对页面进行压缩,以节省空间。