innodb存储引擎

innodb存储引擎
  • 行:最基本的存储单位

    • 数据库读写磁盘的基本单位是页(Page),如果以行为单位,一次I/O操作只能处理一行数据,效率会很低,所以无论是读一行,还是读取多行,都是加载这些行所在的页;
    • lnnoDB将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,页的大小默认为16KB,也就是一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB的内容刷新到磁盘中。
    • 为了减少磁盘i/o,MySQL使用B+树索引,检索一条数据的快慢,主要受树的高度影响的,树越矮,i/o次数越少,通过b+树检索到的也不是目标行数据,而是目标行数所在的页
  • 区(Extent):是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64 个连续的页,也就是64*16KB= 1MB

  • 段(Segment):由一个或多个区组成,段是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在

  • 表空间(Tablespace):是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。

随机i/o的概念:

  • 以机械盘为例,数据最终是保存在磁盘一个个扇区上的,一个扇区写满了,就要换下一个扇区,这时就需要找到目标扇区
  • 如果下一个扇区是紧连着的,就是顺序i/o,如果不是,就需要找个下一个扇区,也就是随机i/o了,这需要更长的物理移动时间,所以减少随机i/o是很重要的
行格式

行格式可分为四种:

  • compact(紧凑的):
  • dynamic (动态的):从5.1开始默认的行格式
  • compressed (压缩的)
  • redundant (冗余)
  • 在创建表的时候,可以指定表的行格式(row_format = 行格式名称),也可以后续修改

行溢出问题:

  • varchar字段,最大的存储范围是65535个字节,默认情况下能存储21,845个字符,但是因为需要用2两个字节记录变长字段的长度,和1个字节来记录null值的标识,所以其实最大可以存放65532个字节
  • 然而65532,已经超过了16k(16384),这样一个页就连一条数据都无法存放,也就是行溢出
  • 所以在compressed和redundant 行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储一部分数据,把剩余的几个数据分散在其他的页中进行分页存储,然后再真实数据处记录下这些页的地址(占用20个字节),从而找到剩余数据所在的页,也被称为页的扩展

dynamic和compressed行格式:

  • 这两个行格式和compact很像,区别就是处理行溢出时有分歧:
  • compressed和dynamic对于存放在BLOB中的数据采用了完全行溢出的方式,真实数据处只存放20字节的地址,实际的数据都存放在其他位置
  • compact和redundant 两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节)
  • compressed的另一个功能就是,存储在其中的行数据会以zlib算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度的数据能够进行非常有效的存储

compact行格式:

  • 除了包含真实记录的数据
    • 真实数据除了真实的列,还会有三个隐藏列:
      • row_id : 行id,可以唯一标识一条记录
      • transaction_id:事务id
      • roll_pointer:回滚指针
  • 还包括一些额外信息:
    • 变长字段长度列表
      • mysql支持一些可变长的数据类型,例如:varchar、text等,字段中存储了多少数据不是固定的,所以在存储真实数据的时候,需要把这些数据占用了多少个字节数也存储起来
      • compact把所有变长字段的真实数据占用的字节长度都存放在记录的开头部分,形成一个变长字段长度列表
    • null值列表
      • compact会把null值的列统一管理起来,存在一个null值列表中,如果表中没有允许null值的列,就没有null值列表
      • 之所以需要存储null值,是因为数据是需要对齐的,如果没有标出null的位置,就有可能在查询数据的时候出现混乱但是如果使用特定值来表示null值,就又会很浪费空间,
      • 所以在行数据头部开辟出一块空间专门来存储该行数据哪些是非空数据,哪些是空数据
      • 用于一个二进制位来表示一个列值是否为空,二进制位的值为1时,代表该列的值位null,为0代表不是null
    • 记录头的信息
      • 两个预留位,都是1bit,没有使用
      • delete_mask,用来标记当前记录是否被删除,0表示没有被删除,1表示删除
        • 之所以采用逻辑删除,不是直接物理删除,移除某条记录后,其他的数据需要再磁盘上重新排列,会导致性能消耗
        • 所有被删除的记录都会存放在垃圾链表,垃圾链表中记录占用的空间被称为可重用空间,如果后面有新的记录插入到表中,有可能会直接复用这些空间
      • min_rec_mask :值为1就代表这条记录是b+树该层非叶子节点的最小记录,其他的记录都是0
      • recode_type:当前的记录类型
        • 0是普通记录
        • 1表示b+树非叶节点记录
        • 2表示最小记录
        • 3表示最大记录
      • heap_no:表示当前记录再本页中的位置
        • mysql会自动给每个页里增加两个记录,由于不是用户插入的所以也叫伪记录,0代表最小记录、1代表最大记录,从值可以知道它们的位置是最靠前的
      • n_owned:每个组中最后一条记录的头信息中会存储该组一共多少条记录,作为n_owned字段
      • next_record:表示当前记录到下一条记录的地址偏移量

redundant :

  • 是5.0版本以前默认的行格式
  • 记录的额外信息包括:
    • 字段长度偏移列表
      • redundant 会把该条记录所有的列,包括隐藏列的长度信息都记录下来
      • 采用的是相邻两个数值的差值来计算各个列的长度,计算列值的长度不如compact直观
    • 记录头信息
      • 相比于compact多了两个记录:记录中的列数量、记录字段长度偏移列表中每个列对应的偏移量
      • 少了recode_type(当前的记录类型)

InnoDB中页被划分为了七个部分:

  • 文件头,描述了页的信息

  • 文件尾,和文件头的检验和一起,用于检验页是否完整

  • 空闲空间,指的是页中还没有使用的空间

  • 用户记录,存储行记录的内容,叶子节点就是真正的数据,用户记录中的数据按照指定的行格式排列,相互之间形成单链表

  • 最大最小记录,是两个虚拟的行记录,mysql会自动给每个页里增加最大和最小记录

  • 页头,记录了很多页的状态信息,例如槽的数量、记录的数量、第一条记录的地址等

  • 页目录,大小不定,存储用户记录的相对位置

文件头和文件尾:

  • 文件头,描述了页的信息,包括:
    • 页号:每一个页都有一个单独的页号,innodb可以通过页号唯一定位一个页
    • 页的类型:代表当前页的类型,例如数据页、Undo日志页和事务数据页等
    • 上一页和下一页,页与页之间是双向链表连接
    • 日志序列号 ,页面被最后修改时日志序列位置
    • 校验和
      • 对于一个很长的字符串,会通过某些算法来计算一个比较短的值来代表这个字符串(类比hash值),这个值就是校验和,再比较两个很长的字符串之前,会先比较校验和,校验和不一样,字符串肯定不一样,节约了比较两个字符串的时间损耗,
      • 对于innodb来说,校验和用于比较两个页是否相同,innodb以页为单位把数据加载到内存中处理,如果页的数据被修改了,就需要同步到磁盘,但如果再同步过程中出现意外,例如停电,就会造成页传输的不完整
      • 可以通过文件头和文件尾检验和的比较来判断一个页是否完整,如果两个值不相等,说明传输有问题,需要重新传输
  • 文件尾,8字节,检验页是否完整
    • 前4个字节代表页的校验和:
    • 后4个字节代表页面被最后修改时对应的日志序列位置

空闲空间:

  • 数据会按照指定的行格式存储到用户记录中,但是一开始生成页的时候,并没有用户记录这个部分
  • 每插入一条记录,都会从空闲空间中申请空间给用户记录,当空闲空间全部被用户记录代替时,就以为着这个页使用完了,如果还有新的记录插入,就需要申请新的页

页目录:

  • 为什么需要页目录
    • 在页中,记录是以单向链表的形式存储的,插入和删除很方便,但是检索效率不高,为了提高检索效率,设计了页目录
    • 排除已经删除的记录,把剩下的所有记录分成几个组:
      • 第一组只有最小记录这一条记录,
      • 最后一组包含了最大记录,会有1到8条记录,其他的组记录数量在4-8条之间,除了第一组数据外,其他组的数据会尽量平分
      • 每个组的最后一条记录的头信息会存储该组中有多少条记录(n_owned),
    • 页目录按照顺序存储每一组的最后一条记录(最大的记录)的地址偏移量,每组的地址偏移量也成为槽(slot)
    • 查找时可以通过二分法快速确定记录所在的槽,并找到槽所在分组中主键值最小的那条记录,然后通过next_record(当前记录到下一条记录的地址偏移量)属性遍历槽中的记录
  • 如何确定页目录的分组个数
    • 初始情况下,一个数据页里只有最小和最大记录,分属于两个组
    • 之后每插一条记录,都会从页目录中找到主键值比记录的主键值大并且差值最小的槽,然后把该槽的记录数值加1,直到组中的记录数等于8
    • 如果一个组的记录数达到8,再插入新的记录,就会新增一个组,然后把原组中放入记录拆分一部分到新的组,一个组中4条记录,一个组中5条记录

从页的角度看b+树的检索:

  • 从b+树的根开始逐层检索,直到找到叶子节点,也就是对应的数据页
  • 然后将页加载到内存中,通过页目录中的槽采用二分查找到对应的组
  • 然后遍历组中的链表找到记录

普通索引和唯一索引在效率上的差距:

  • 唯一索引有唯一性约束,只有找到唯一那条的数据,就不需要再继续查询了
  • 而普通索引在找到符合条件的记录后,还需要继续查找,然而因为数据页的大小是16K,如果符合条件的数据都在这个页上,多查找几条数据的时间可以忽略不记,但如果符合条件的数据在多个数据页上就会比较耗费时间
区、段、表

区(Extent)

  • b+树的每一层中的页都会形成一个双向链表,如果以页为单位来分配存储空间,双向链表两个相邻的页的位置可能会非常远,
  • 如果是范围查询,需要定位最左和最右记录,而如果相邻的页离得非常远,也就是随机i/o了,这样的效率很差,所以应该尽可能的让链表中相邻的页的物理位置也相邻,这样在范围查找的时候才能使用顺序i/o
  • 所以引入了区的概念,一个区就是物理位置上连续的64个页,也就是 64*16 为1M的空间
  • 所以在数据量比较大的时候,为某个索引分配空间的时候就不在以页为单位,而是以区为单位,甚至在数据量特别大的时候,会一次性分配多个区。虽然可能会造成空间的浪费,但是能消除很多随机i/o,功大于过

段(Segment):

  • 叶子节点和非叶子节点都是页,如果一个区同时存在叶子节点和非叶子节点,范围查找时,会需要扫描更多的区
  • 所以innodb把叶子节点和非叶子节点分开进行存储,存放叶子节点的区的集合为一个段,存放非叶子节点的区的集合是另一个段,
  • 所以一个innodb索引会产生两个段,一个叶子节点段(数据段),一个非叶子节点段(索引段)
  • 此外还会为存储一些特殊数据而定义段,例如回滚段
  • 段并不是一个连续的物理上的空间,而是逻辑上的概念,由存储引擎自身来管理,包含若干零星的页面,和一些完整的区

碎片区:

  • innodb表一定有一个聚簇索引,一个索引有两个段,段是以区为单位来申请的,
  • 如果一个段最小就是一个区的大小,也就是1M,即使数据量非常小,一个索引也要占用2M的空间,如果在接着加索引,仍然需要2M,就会造成很大的浪费
  • 所以在数据量非常小的情况下 ,以完整的区为单位来分配空间,是很大的浪费,所以提出了碎片区的概念
    • 在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是可以用于不同的目的,例如同时存储叶子节点和非叶子节点
    • 所以,碎片区不属于任何一个段,直属于表空间
  • 为某个段分配空间的策略是:
    • 在刚开始向表中插入数据时,段是从某个碎片区以单个页面(16K)为单位来分配存储空间的
    • 当段已近占用了32个碎片区页面后,就会申请完整的区为单位来分配存储空间

区的分类:

  • 空闲区:还没有用到区中的任何页面
  • 有剩余空间的碎片区
  • 没有剩余空间的碎片区
  • 附属于某个段的区,这个区属于段,其他的区都是碎片区,直属于表空间

表空间

  • 表是innodb存储引擎逻辑架构最高层,所有的数据都存放在表空间中
  • 表空间可以分为:
    • 系统表空间
      • mysql进程只有一个系统表空间,有一些系统表,会额外记录整个系统的信息
      • 用户是不能直接访问这些表的
    • 独立表空间
      • 每个表都有一个独立的表空间,数据和索引信息都会保存在表空间中
      • 内部结构就是:段、区、页
      • 在8.0以后,frm文件合并到ibd文件,所以8.0初始的ibd文件比5.7大16K
    • 撤销表空间
    • 临时表空间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值