MySQL-关于Innodb
MySQL-关于Innodb
xukunInnoDB中聚簇索引和非聚簇索引的区别
InnoDB的底层是用B+树
实现的,所以聚簇索引和非聚簇索引在默认情况下也是使用B+树
实现,但是存在一定的差别,如下表所示:
区别 | 聚簇索引 | 非聚簇索引 |
---|---|---|
叶子节点存储内容 | 完整数据 | 主键、索引列 |
在表中是否唯一 | 是 | 否 |
适用场景 | 范围查询、排序操作 | 快速查找数据 |
因为B+树的叶子节点之间是通过双向链表连接的,所以对于存储了数据行的聚簇索引来说在特定范围内进行数据查询和对数据进行排序操作不用修改数据的结构,IO次数减少查询速度快。
而对于非聚簇索引,因为存储的是主键和索引列,想要通过非聚簇索引来查找完整的数据内容会增加回表的次数,造成IO次数的开销,因此查找完整的数据比较慢,但是用于快速查找特定数据,根据主键和索引列匹配数据会比聚簇索引快。
扩展:
Innodb数据存储结构
从Mysql5.5版本开始,InnoDB是默认的表存储引擎。其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读、同时被设计用来最有效的利用以及使用内存和CPU。
innodb的存储格式从行,页,区,段,再到表空间,环环相扣。接下来就介绍存储格式
innoDB 是一个将表中的数据存储到磁盘上的存储引擎,在真正处理数据的时候,是在内存中处理的,因此需要将数据从磁盘读取到内存中,在处理写入或者修改操作之后,也需要进行一个刷盘的操作,将数据从内存刷新到磁盘上。因此在磁盘上的数据,也是其对应的存储结构的,并且其innodb是以页单位存储数据的,一页数据为16kb。
innodb磁盘页存储数据方式
行格式
COMPACT行格式
在innodb中,主要通过行格式这种方式来存储数据,并且该殷勤设计了四种不同类型的行格式,分别是Compact,Redundant(废弃),Dynamic和Compress。这四种在本质上,没有太大的差别,因此以下主要是讲解这个Compact这种类型
一条完整的记录行,包括额外信息和真实数据两部分,接下来就相继介绍这些部分。
额外信息
变长字段长度列表:
MySQL 支持一些变长的数据类型,比如 VARCHAR(M) 、 VARBINARY(M) 、各种 TEXT 类型,各种 BLOB 类型,这些数据类型的列称为 变长字段 ,变长字段中存储多少字节的数据是不固定的,所以我们在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。
这些长度的描述是逆序存放的,这样一条记录在读取真实数据的时候向右读取到的第一列,同时再向左读取其长度。比如 列1长度04,列2长度03,这样逆序排列。
值的注意的是,null值字段的长度是不存储的。
null值列表
我们常常说,建议字段设置成非空,这是因为如果字段可为空的话,mysql需要额外开辟一个字节来存字段是否为空的信息,也就是null值列表。
但是实际上字段是否为null的存储其实很小的啦。一个字节有8位,就能描述8个可空的字段了,也是通过逆序的方式,每个位都可以表示一个字段,0为非空1为空。
假设c3和c4字段都为空,那么效果就是上图所示。如果一行数据可空字段超过了8个,那么就要用2字节来描述了。
记录头信息
记录头信息由固定的5个字节组成,也就是40位。
我们只需要关心几个常见字段,
delete_mask 1bit 标记该记录是否被删除 (底层记录删除只是修改该状态,这样对mvcc才是可见的)
min_rec_mask 1bit B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4bit 表示当前记录拥有的记录数,这个在页中的每个槽的最后记录都会记录该值
record_type 3bit 表示当前记录的类型, 0 表示普通记录, 1 表示B+树非叶子节点记录, 2 表示最小记录, 3 表示最大记录
next_record 16bit 表示下一条记录的相对位置偏移量(存储形成单向链表,才提高了范围查询的速度)
真实数据
对于字段来说,除了我们自己定义的字段, MySQL 会为每个记录默认的添加一些列(也称为 隐藏列 ),具体的列如下:
DB_ROW_ID:该字段是一个非必须字段。该字段主要就是用来表示表中的聚簇索引,如果表中没有建主键和唯一索引时,那么这个隐藏的字段就作为全局的主键索引。
DB_TRX_ID:这个id表示的是一个事务id,在用了事务时,他就会用来记录对应的数据。
DB_ROLL_PTR:这个表示回滚指针,就是redolog的版本日志链,如果发生数据回滚时,就会用到该回滚指针。详情可以查看这个mvvc篇。
InnoDB存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列) 这时候row_id就变成了我们的聚簇索引。这些字段在后续的mvcc中提供了巨大的作用。
行溢出
一般情况下一个页是16k,如果我们的字段设置了很多,同时字段的长度又很大,就会出现一个页都存放不了一行的尴尬情况,称为行溢出。在compact行格式下,针对行溢出,只会存储该列的前 768 个字节的数据和一个指向其他页的地址,然后把剩下的数据存放到其他页中。
Dynamic行格式
Dynamic行格式是5.7默认的行格式,他和compact的差别在于对行溢出的处理。dynamic格式会将溢出字段的所有数据都放在别的页,字段只存该页地址。
在定义一个varchar字段的时候,varchar可以定义的最大长度为65535,因此可以设置一个20000长度的字段
但是mysql是以页为单位存储数据的,一页的数据为16kb,即 16 x 1024 = 16384个字节,那么就会出现数据溢出的现象,即一页数据不能全部存储完全部数据。那么这几个不同的类型就会有不同的处理方法,Compact会将前768个字节存储在本页内,剩余的存储在其他页面中,然后通过指针找到对应的存储在其他页的数据;而这个Dynamic和Compress两种类型就是直接将全部的数据直接存储在其他的页面上,然后该页面内只存储指向其他页面的指针。
页格式
页是InnoDB 管理存储空间的基本单位,一个页的大小一般是 16KB 。 InnoDB 为了不同的目的而设计了许多种不同类型的 页 ,比如存放表空间头部信息的页,存放 Insert Buffer 信息的页,存放 INODE 信息的页,存放 undo 日志信息的页等等等等。我们聚焦的是那些 存放我们表中记录的那种类型的页,官方称这种存放记录的页为索引( INDEX )页。
一个16kb的页又被分为多个部分,不同的部分提供不同的功能。
如上图,主要由File Header,Page Header,Infimum + Supremum,User Records,Free Space,Page Directory,File Trailer文件尾部等部分组成。
File Header:文件头,主要存储一些页面的通用信息,所有的页都有这个文件头。
Page Header:页面头,主要存储数据页专有的一些信息。
Infimum + Supremum:最小记录和最大记录,由内部所维护的虚拟记录。
User Records:用户记录,实际存储的行记录内容。上面的那些数据就是存在这个位置的
Free Space:空闲空间,页面中还没有使用完的空间
Page Directory:页面目录,某些数据的相对位置的记录
File Trailer:文件尾部
其中,大小不确定的user records就是用来存我们的行记录的,而随着记录增加userrecords越来越大,free space越来越小。
像这样:
Infimum + Supremum
每个页都会有一条最小记录,和最大记录。这是用来辅助我们定位的,他们起到了关键的作用,比如我们的临键锁,需要锁(100,+无穷)的位置,就是靠在最后一条suprenum记录上加锁的。
它们拥有和普通记录一样的记录头,具体如下:
具体的作用呢,就是将我们页内的所有记录给串联起来,统一管理,这里就用到了记录头的next-record指针
这样我们的记录就从小到大形成了一个单向链表,方便了之后的检索。如果现在需要删除一条记录2,会怎么做呢?
它做了几件事:
- 第2条记录并没有从存储空间中移除,而是把该条记录的 delete_mask 值设置为 1
- 第2条记录的 next_record 值变为了0,意味着该记录没有下一条记录了
- 第1条记录的 next_record 指向了第3条记录
- 还有一点你可能忽略了,就是 最大记录 的 n_owned 值从 5 变成了 4
上面的这些记录头,都是行格式介绍的,现在知道具体用处了吧。
这里注意一点,下一条记录的指针指向的都是记录头和真实数据中间的那个节点。这就是为啥null值和变长字段都是逆序排放的了,从中间向两边散开,分别找到对应一个字段的情况就最快。
Page Directory(页目录)
了解到了页存储数据的格式是个单向链表,那你会想到,单向链表的查询复杂度不是O(n)吗,那不是贼慢,不是说b+树么?还没到b+树,别着急。先了解页内部的查询,也就是本节的主角,页目录。
为了提高页内的查询速度,我们采用了一个槽来对记录行进行分组
InnoDB对每个分组中的记录条数是有规定的:对于最小记录所在的分组只能有 1 条记录, 最大记录所在的分组拥有的记录条数只能在 18 条之间,剩下的分组中记录的条数范围只能在是 48 条之间。
所以当记录数足够多的时候是这样的:
有了很多个槽,这些槽形成了页目录。查询一条记录的时候,我们可以先通过二分法,找到记录对应的槽,然后再从槽的最小记录开始,通过单向链表遍历,不超过8行记录,就能找到我们想要的记录了。
Page Header(页面头部)
为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第 一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫 Page Header 的部分
File Header(文件头部)
File Header 针对各种类型的页都通用,也就是说不同类型的页都会以 File Header 作 为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、 下一个页是谁等问题。我们的页在叶子节点中双向链表相连,靠的就是这个。
File Trailer
为了加快速度,页都会被加载进buffer pool缓存中修改,然后再以一定的频率刷新回磁盘,那如果刷新一半就断电了怎么办?
为此,页头部和页尾部都存放了当前页的校验和,如果头尾的校验和不一致,说明刷盘中途出现了问题。
该部位分为八个字节,两个部分
前一个部分4个字节,存校验和
后一部分4个字节,存该页刷盘的时候对应的redolog中的LSN(后面会提)。
区格式
由于页实在是太多了,为了更好的管理,mysql提出了区的概念。连续的64个页就是一个区也就是区默认1MB的空间。这样就能够实现大多的页在申请的时候都是连续的了,会更好的加快我们查询寻址的数据,也是提高了我们的范围检索速度,减少随机io。
段格式
我们想要的顺序检索,实际上是叶子节点上的顺序,所以mysql又提出了段的概念,分为了索引段和数据段,做更好的隔离。
常见的段有:
数据段:B+树的叶节点。
索引段:B+树的非叶节点。
回滚段:即rollback segment,管理undo log segment。