0
点赞
收藏
分享

微信扫一扫

MySQL行格式

seuleyang 2022-02-22 阅读 172

1. 前言

MySQL架构分为Server层和存储引擎层,Server层负责接收处理客户端指令,一旦涉及到数据的读取和写入操作,最终是需要调用存储引擎提供的接口来完成的。在MySQL的整个生态里,除Memory外,绝大多数存储引擎都是将数据存储在磁盘上的,例如常用的InnoDB、MyISAM。

大家有没有思考过这样一个问题,我们提交Insert语句,MySQL会帮我们把数据保存下来,当我们查询的时候它又能把数据再返回给我们,那它底层是按照什么格式存储数据的呢?

我们以一条条记录为单位向表中插入数据,MySQL也是以「记录」为单位进行存储的,记录在磁盘上存储的格式,就是我们今天要讨论的「行格式」。

行格式可以在创建表的时候指定,语法如下:

CREATE TABLE 表名 (...) ROW_FORMAT = 行格式

也可以对已经存在的表进行行格式的变更:

ALTER TABLE 表名 ROW_FORMAT = 行格式

不同行格式对数据读写的影响?
有的行格式设计的不紧凑,同样一条记录会占用更多的磁盘空间,意味着一个页里包含的记录就少了,进而会影响DML操作的性能,查询可能需要更多的磁盘IO。有的行格式还会对数据做压缩处理,磁盘IO的效率高了,但是会消耗更多的CPU资源。所以,行格式的选择对数据读写的效率是有影响的,具体如何选择,需要根据场景而定。MySQL5.7版本,默认使用DYNAMIC行格式。

2. InnoDB行格式

InnoDB存储引擎目前共支持四种行格式,如下表:

行格式紧凑存储增强的可变长度列存储大索引键前缀支持支持压缩
REDUNDANT
COMPACT
DYNAMIC
COMPRESSED

其中REDUNDANT是一种非常古老的行格式,现在应该没什么人用了,它存在的意义是与旧版本的MySQL做兼容。COMPACT行格式需要重点了解,DYNAMIC和COMPRESSED不过是它的另外两个变体。我们就先看老的再看新的,顺便感受一下MySQL做了哪些优化。

2.1 REDUNDANT

REDUNDANT属于「非紧凑」的行格式,这意味着它比较占用磁盘空间,间接导致查询时可能需要更多的磁盘IO。占空间、效率不高,这就是它被淘汰的主要原因。

REDUNDANT存储记录的格式如下图:
image.png
1、字段长度偏移列表
REDUNDANT没有区别对待定长和变长字段,将所有列占用的存储空间都逆序存放在字段长度偏移列表中。根据字段的偏移量就可以定位到字段的存储位置,和下一个偏移量的差值可以计算出字段的长度,从而取出字段的完整信息。
2、记录头信息
REDUNDANT记录头信息固定占用6字节,即48个比特位,每个比特位代表的含义如下表:

名称大小(Bit)说明
预留位11没有使用
预留位21没有使用
deleted_flag1记录删除标记
min_rec_flag1B+树非叶子节点的最小目录项标记
n_owned4同一页内同一组里最大的记录会记录组里的记录数量,其余记录该值为0
heap_no13当前记录在页面堆里的相对位置
n_field10记录中列的数量
1byte_offs_flag1标识字段长度偏移列表里用1字节还是2字节存储长度
next_record16下一条记录的相对位置

3、使用几个字节来记录字段长度偏移量?
当记录所有列的总长度不超过127时,使用1字节存储,因为总长度都没拆过127,单个字段的长度肯定不会超过127。列总长度大于127时,使用2字节存储。2字节最多能表示65535,有没有可能一行记录占用的空间超过了65535呢?是有可能的,但此时该列肯定属于「溢出列」了,记录的真实数据处只会保存前768字节的数据+20字节的指针,剩余的数据则存储在专门的「溢出页」中。
4、如何处理NULL?
REDUNDANT没有专门的「NULL值列表」,那它是如何处理NULL值的呢?还记得「字段长度偏移列表」吗?1字节最大能表示255,为啥超过127就开始使用2字节呢?原因就在于,REDUNDANT会把第0位用来标记是否为NULL,第0位是1则代表值为NULL,是0就不为NULL。
5、定长列和变长列处理NULL值的区别?
如果定长列存储的是NULL值,则NULL值也会占用存储空间,数据全部用0x00字节填充。例如char(10)就会占用10个字节(与字符集有关,utf8则直接占用30字节),这样做的好处是,以后update该列时,可以直接复用这一块空间。如果变长列存储的是NULL值,则NULL值本身不占空间。

综上所述,可以看出,REDUNDANT设计的简单粗暴,正因如此也导致它比较浪费磁盘空间,属于非紧凑的行格式。我们接下来看COMPACT,你就知道它设计的有多紧凑了。

2.2 COMPACT

COMPACT行格式也将记录分为两部分,如下图所示:
image.png
1、变长字段长度列表
针对VARCHAR、TEXT、BLOB这类变长字段,列中实际存储了多少数据是不固定的,因此除了要把数据本身存下来,还需要记下它的长度。COMPACT将变长列的实际长度按照字段的顺序,逆序存储在变长字段长度列表里。

  • 为啥逆序存储?

记录头信息里有一个指针,将一条条记录串联成单向链表。指针指向的位置并不是一条完整记录的起始位置,而是图中「记录的真实数据」的起始位置。这样的好处是,往右读就是真实数据,往左读就是头信息,根据计算机的局部性原理,更容易提高二者缓存的命中率。

  • 使用几个字节存储长度?

这与列使用的字符集有关,假设该字符集最多使用X个字节表示一个字符,VARCHAR(N)最多存储N个字符,占用的字节数最多是X*N,假设该列实际长度是L。如果X*N<=255,则使用1字节,因为最多占用的字节数不会超过255,1个字节就足够了。 如果X*N>255 && L<=127,使用1字节存储。如果X*N>255 && L>127就使用2字节存储。

  • X*N>255时,如何判断使用几个字节存储长度?

X*N>255时,长度可能用1个或2个字节来表示,REDUNDANT的方案是在头信息里记下来,而COMPACT直接在长度本身做了个小把戏。1字节能表示的最大值是255,为啥L超过127就采用2字节存储?因为MySQL把第0个比特位用来标记是否采用2字节存储,如果第0位为0则采用1字节存储,第0位为1则采用2字节存储。

  • CHAR(N)算不算变长?

我们总说“char是定长的,varchar是变长的”,那是不是char类型的列就不需要记录变长字段长度呢?答案是:要看你使用的字符集!如果是ascii字符集,所有字符都固定占用1字节,那就不需要记录。如果是utf8,一个字符占用的字节数为1~3,这种不固定的字符集,就需要存储了变长字段长度了。例如CHAR(10)使用utf8字符集,占用的存储空间范围是10~30,列的实际长度还是不确定的。
2、NULL值列表
没有限制NOT NULL的列是可以设为NULL的,COMPACT是如何存储NULL值的呢?答案是:不存储!对于NULL值列,它是不会在「记录的真实数据」处占用任何空间的,仅仅是在「NULL值列表」用1个比特位来标记该列值为NULL。「NULL值列表」要求必须是整数个字节,不足的高位补0处理,按照列的顺序逆序存储,1代表NULL,0代表非NULL。
3、记录头信息
COMPACT记录头信息占用固定的5字节,即40个比特位,对应的含义如下:

名称大小(Bit)说明
预留位11没有使用
预留位21没有使用
deleted_flag1记录删除标记
min_rec_flag1B+树非叶子节点的最小目录项标记
n_owned4同一页内同一组里最大的记录会记录组里的记录数量,其余记录该值为0
heap_no13当前记录在页面堆里的相对位置
record_type3记录类型。0:普通记录,1:B+树非叶子节点目录项记录,2:Infimum记录,3:Supremum记录.
next_record16下一条记录的相对位置
  • deleted_flag

DELETE命令删除记录,并不会真的将它从磁盘中删除,而是仅仅打一个标记,然后把该条记录加入到「垃圾链表」里,垃圾链表占用的空间称为「可重用空间」,以后如果在这个位置插入新的记录就可以重用这部分空间了。如果一个页内所有的记录都被删除了,那么这个页就称为「可重用的页」。

  • min_rec_flag

InnoDB引擎组织数据的形式采用了B+树,用户记录存储在叶子节点,目录项(也可叫索引项)存储在非叶子节点,一个个节点就是一个个页,同一个非叶子节点内最小的目录项该比特位为1,其余均为0。

  • n_owned

InnoDB引擎页大小默认是16KB,同一个页内可能会存储很多的用户记录,甚至上千条。为了提高页内的检索效率,InnoDB会将记录划分为多个不同的组,组内记录值最大的一条称为“大哥”,其余的都是“小弟”,“大哥”会利用该属性来记录组内的记录数量,各个组的“大哥”的值会按照顺序被记录在页内的「Page Directory」位置。

  • heap_no

用户记录存储在页的「User Records」部分,MySQL将这部分结构称作堆(Heap),每申请一块记录空间,都会为其分配一个heap_no,越靠前的记录heap_no越小,越靠后的记录heap_no越大。

  • record_type

记录类型,目前有4种:

record_type说明
0用户自己插入的记录,或二级索引叶子节点记录。
1B+树非叶子节点目录项记录,冗余的索引项记录。
2页内虚拟的最小记录:Infimum
3页内虚拟的最大记录:Supremum
  • next_record

用户记录会根据主键值排序并构建一条单向链表,链表就是通过该属性来构建的。它代表当前记录的真实数据到下一条记录的真实数据的距离,值为正数代表下一条记录在后面,值为负数代表下一条记录在前面。MySQL规定,页中Infimum的下一条记录是本页中主键值最小的记录,主键值最大的记录next_record一定指向Supremum。

综上所述,COMPACT设计的比REDUNDANT要紧凑的多。能用1字节绝不用2字节,能用bit表示就绝不用byte表示。

2.3 DYNAMIC和COMPRESSED

MySQL5.7默认使用的行格式就是DYNAMIC了,它和COMPACT非常像,是COMPACT的一个变体。区别是在处理「溢出列」时,COMPACT会在「真实数据」处存储前768字节数据+20字节指针,而DYNAMIC只会存储20字节指针,溢出列的所有数据全部存储在「溢出页」中。
COMPRESSED的特点是它可以使用压缩算法对页面进行压缩,包括「溢出页」,这对于像长文本、TEXT、BLOB类型的数据来说,可以极大的节省空间。但是检索数据时,必须先解压才可以进行后续操作,这会消耗更多的CPU资源,CPU和磁盘IO两者的开销,需要各位去权衡。

3. 总结

绝大多数存储引擎将数据持久化存储在磁盘中,行格式决定了记录在磁盘上的存储格式。InnoDB引擎目前共支持四种行格式,古老的REDUNDANT设计的简单粗暴,缺点是会占用更多的磁盘空间,间接影响了DML操作的效率,查询需要更多的磁盘IO。COMPACT行格式设计的非常紧凑,也更加的复杂,能用Bit表示就绝不用Byte,是一个非常经典的行格式。DYNAMIC和COMPRESSED是COMPACT的两种变体,在处理溢出列时稍有不同,后者支持对页面进行压缩,通过消耗CPU算力还换取磁盘IO的性能。

举报

相关推荐

0 条评论