MySQL 03_MySQL存储引擎详解

一、MySQL 存储引擎简介

1.1 存储引擎简介

MySQL 服务器架构 把 连接管理查询缓存语法解析查询优化 这些并不涉及真实数据存储的功能划分为 MySQL server 的模块,把 存取数据 的功能划分为 存储引擎 的模块。

在Mysql server完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的API,获取到数据后返回给客户端。

存储引擎 就是指表的类型,其实存储引擎以前叫作表处理器,后来才改名为 存储引擎,它的功能就是接受上层传下来的指令,然后对表中的数据进行提取或写入操作。

使用 show engines; 可查询服务器支持的存储引擎列表, MySQL 8支持9种存储引擎,分别为MEMORY、MRG_MYISAM、CSV、FEDERATED、PERFORMANCE_SCHEMA、MyISAM、InnoDB、BLACKHOLE 和 ARCHIVE :

  • Engine参数:表示存储引擎名称。
  • Support参数:表示MySQL数据库管理系统是否支持该存储引擎: YES表示支持,NO表示不支持。
  • DEFAULT:表示系统默认支持的存储引擎。
  • Comment参数:表示对存储引擎的评论。
  • Transactions参数:表示存储引擎是否支持事务:YES表示支持,NO表示不支持。
  • XA参数:表示存储引擎所支持的分布式是否符合XA规范: YES表示支持,NO表示不支持。代表着该存储引擎是否支持分布式事务。
  • Savepoints参数:表示存储引擎是否支持事务处理的保存点: YES表示支持,NO表示不支持。也就是说,该存储引擎是否支持部分事务回滚。

1.2 查看/设置MySQL系统默认的存储引擎

1、查看默认的存储引擎:

1
2
3
show variables like '%storage_engine%'; 
#
SELECT @@default_storage_engine;

2、修改默认的存储引擎

使用 SQL 语句切换:

1
SET DEFAULT_STORAGE_ENGINE=MyISAM;

这会立即将 DEFAULT_STORAGE_ENGINE 模式切换为指定的模式。 这种修改在 MySQL 服务重启后会失效,因此需要在重启前进行相应的配置修改。

修改my.cnf文件:

1
default-storage-engine=MyISAM 

然后重新启动 MySQL 服务,使新的 efault-storage-engine 模式生效。

3、设置表的存储引擎

存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为不同的表设置不同的存储引擎,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

创建表时指定存储引擎:

1
2
3
CREATE TABLE 表名(
    建表语句; 
) ENGINE = 存储引擎名称;

如果在创建表的语句中没有显式指定表的存储引擎的话,那就会默认使用InnoDB作为表的存储引擎。

修改表的存储引擎:

1
ALTER TABLE 表名 ENGINE = 存储引擎名称;

二、InnoDB 引擎详解

2.1 具备外键支持功能的事务存储引擎

MySQL从3.23.34a开始就包含InnoDB存储引擎,MySQL5.5及之后的版本,默认采用InnoDB引擎。

InnoDB是MySQL的默认事务型引擎,它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。

除了增加和查询外,还需要更新、删除操作,那么,应优先选择InnoDB存储引擎。

除非有非常特别的原因需要使用其它的存储引擎,否则应该优先考虑InnoDB引擎。

数据文件结构:

  • 表名.frm 存储表结构(MySQL8.0时,合并在表名.ibd中)
  • 表名.ibd 存储数据和索引

InnoDB是为处理巨大数据量的最大性能设计。

  • 在以前的版本中,字典数据以元数据文件、非事务表等来存储。现在这些元数据文件被删除了。比如:.frm,.par,.trn,.isl,.db.opt等都在MySQL8.0中不存在了。

对比MyISAM的存储引擎,InnoDB写的处理效率差一些,并且会占用更多的磁盘空间以保存数据和索引。

MyISAM只缓存索引,不缓存真实数据;InnoDB不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响。

2.2 InnoDB数据存储结构 - 页(page)详解

1、数据库的存储结构:页

索引结构提供了高效的索引方式,不过 索引信息以及数据记录都保存在文件上的,确切说是存储在页结构中。另一方面,索引是在存储引擎中实现的,MySQL服务器上的存储引擎负责对表中数据的读取和写入工作。不同存储引擎中存放的格式一般不同的,甚至有的存储引擎比如Memory都不用磁盘来存储数据。

2、磁盘与内存交互基本单位:页

InnoDB将数据划分为若干个页,InnoDB中页的大小默认为16KB。

作为磁盘和内存之间交互的基本单位,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是 页(Page),数据库I/O操作的最小单位也是 页(Page),一个页中可以存储多行记录。

Tips: 记录是按照行来存储的,但是数据库的读取并不以行为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。

3、页结构概述

页a、页b、页c … 页n 这些页可以不在物理结构上相连,只要通过双向链表相关联即可。

每个数据页中的记录会按照主键值从小到大的顺序组成一个单向链表,每个数据页都会为存储在它里边的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应的分组中的记录即可快速找到指定的记录。

4、页的大小 不同的数据库管理系统(DBMS)的页大小不同,比如在MySQL的InnoDB存储引擎中,默认页的大小是16KB

1
2
# 通过下面的命令来进行查看;
show variables like '%innodb_page_size%';

5、页的上层结构

如下图所示: 区(Extent) 是比页大一级的存储结构,在InnoDB存储引擎中,一个区会分配64个连续的页,因为InnoDB中的页大小默认是16KB,所以一个区的大小是64*16KB=1MB;

段(Segment) 由一个或多个区组成,区在文件系统是一个连续分配的空间(在InnoDB中是连续的64个页),不过在段中不要求区与区之间是相邻的。

段(Segment) 是数据库中的分配单位,不同类型的数据库对象以不同的段形式存在。当创建数据表、索引的时候,就会相应创建对应的段,比如创建一张表时会创建一个表段,创建一个索引时会创建一个索引段。

表空间(Tablespace) 是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。

6、页的内部结构

页(page) 如果按类型划分的话,常见的有 数据页(保存 B+ 树节点)系统页Undo 页事务数据页等,数据页是最常使用的页。

数据页的16KB大小的存储空间被划分为七个部分,分别是: 文件头(File Header)页头(Page Header)最大最小记录(Infimum+supermum)用户记录(User Records)空闲空间(Free Space)页目录(Page Directory)文件尾(File Tailer) 这七部分的作用如下表:

名称 占用空间大小 说明
File Header 38字节 文件头,描述页的信息
Page Header 56字节 页头,描述页的状态信息
Infimum+supermum 26字节 最大 和 最小记录,这是两个虚拟的行记录
User Records 不确定 用户记录,存储行数据(记录)内容
Free Space 不确定 空闲空间,页中还没有被使用的空间
Page Directory 不确定 页目录,存储用户记录的相对位置
File Tailer 8字节 文件尾,用于校验页是否完整

1) 文件头部(File Header)和文件尾部(File Tailer)

File Header(文件头) 不同类型的页都会以 File Header 作为第一个组成部分,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页,下一个页是谁等,所有的数据页会组成一个双链表

文件头(File Header) 部分占用固定的38字节,是由下表这些内容组成的:

名称 占用空间大小 描述
FIL_PAGE_SPACE_OR_CHKSUM 4字节 页的校验和(checksum值)
FIL_PAGE_OFFSET 4字节 页号
FIL_PAGE_PREV 4字节 上一个页的页号
FIL_PAGE_NEXT 4字节 下一个页的页号
FIL_PAGE_LSN 8字节 页面被最后修改时对应的日志序列位置
FIL_PAGE_TYPE 2字节 该页的类型
FIL_PAGE_FILE_FLUSH_LSN 8字节 仅在系统表空间的一个页中定义,代表文件至少被刷新到了对应的LSN值
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4字节 页属于哪个表空间

FIL_PAGE_OFFSET(4字节):每一个页都有一个单独的页号,InnoDB通过页号可以唯一定位一个页。

FIL_PAGE_TYPE(2字节):代表当前页的类型,其值如下图表: 存放记录的数据页的类型其实是FIL_PAGE_INDEX, 也就是所谓的 索引页 。

数据页的链接实现

InnoDB都是以页为单位存放数据的,如果数据分散到多个不连续的页中存储的话需要把这些页关联起来,FIL_PAGE_PREVFIL_PAGE_NEXT 就分别代表本页的上一个和下一个页的页号。这样通过建立一个双向链表把许许多多的页就都串联起来了,保证这些页之间 不需要是物理上的连续,而是逻辑上的连续。

检验页的完整性

Tips: 什么是校验和?

  • 校验和 就是对于一个很长的字节串来,通过某种算法来计算一个比较短的值来代表这个很长的字节串,这个比较短的值就称为校验和;
  • 在比较两个很长的字节串之前,先比较这两个长字节串的校验和,如果校验和都不一样,则两个长字节串肯定是不同的,所以省去了直接比较两个比较长的字节串的时间损耗;

InnoDB存储引擎以 为单位把数据加载到内存中处理,如果该页中的数据在内存中被修改了,那么在修改后的某个时间需要把数据同步到磁盘中, 在同步数据到磁盘时,可能在同步了一部分的时候宕机了,这将造成了该页同步(传输)的不完整。

为了检测一个页是否完整(也就是在同步的时候有没有发生只同步一部分的情况),这时可以通过文件尾的校验和(checksum 值)与文件头的校验和做比对,如果两个值不相等则证明页的同步传输有问题,需要重新进行同步传输,否则认为页的传输已经完成。

Tips: 文件头部和文件尾部都有:FIL_PAGE_SPACE_OR_CHKSUM

如果需要重新进行同步传输,则根据 文件头中的 FIL_PAGE_LSN(8字节)页面被最后修改时对应的日志序列位置(英文名是:Log Sequence Number)相关信息找回需要重新同步的数据。

File Trailer(文件尾部)

文件尾(File Tailer)前4个字节代表页的校验和:这个部分是和File Header中的校验和相对应的; 文件尾(File Tailer)后4个字节代表页面被最后修改时对应的日志序列位置(LSN):这个部分也是为了校验页的完整性的,如果首部和尾部的LSN值校验不成功的话,就说明同步过程出现了问题;

2)空闲空间、用户记录 和 最小最大记录

User Records (用户记录)

存入表的记录会按照指定的行格式一条一条存储到(摆放在)页的 User Records部分,相互之间形成单链表。

Free Space (空闲空间)

存储的记录会按照指定的行格式存储到User Records部分,但是在一开始生成页的时候,其实并没有User Records这个部分,每当插入一条记录,都会从Free Space部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到User Records部分,当Free Space部分的空间全部被User Records部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。

Infimum + Supremum(最小最大记录)

记录可以比大小,对于一条完整的记录来说,比较记录的大小就是比较主键的大小。比方说我们插入的4行记录的主键值分别是:1、2、3、4,这也就意味着这4条记录是从小到大依次递增。

InnoDB规定的最小记录与最大记录这两条记录的构造十分简单,都是由5字节大小的记录头信息和8字节大小的一个固定的部分组成的。 这两条记录不是DB用户定义的记录,所以它们并不存放在页的User Records部分,它们被单独放在page head 之后、user record 之前的一个称为 Infimum + Supremum 的部分:

3)页目录 和 页头

Page Directory(页目录)

在页中,记录是以单向链表的形式进行存储的。单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此在页结构中专门设计了 页目录(Page Directory) 这个模块,专门为记录做一个目录,通过二分查找法的方式进行检索,提升效率。

Tips:查找方式一:顺序查找

  • 从Infimum记录(最小记录)开始,沿着链表一直往后查找,可能找到或者找不到,在找的时候还能投机取巧,因为链表中各个记录的值是从小到大顺序排序的,所有当链表的某个节点代表的记录的主键值大于要查找的主键值时,就可以停止查找了,因为该节点后边的节点的主键值依次递增;
  • 如果一个页中存储了非常多的记录,这么查找性能很差;

查找方式二;使用页目录,二分法查找

  • 将所有的记录分成几个组,这些记录包括最小记录和最大记录,但不包括标记为 已删除 的记录;
  • 第 1 组,也就是最小记录所在的分组只有 1 个记录;最后一组,就是最大记录所在的分组,会有 1-8 条记录;其余的组记录数量在 4-8 条之间; 这样做的好处是,除了第 1 组(最小记录所在组)以外,其余组的记录数会尽量平分;
  • 在每个组中最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段;
  • 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录;

假设page_demo表中正常的记录共有6条,InnoDB把它们分成两组,第一组中只有一个最小记录,第二组中是剩余的5条记录,如下图: 图中需要注意几点:

  • 现在页目录部分中有两个槽,也就意味着记录被分成了两个组,槽1中的值是112,代表最大记录的地址偏移量(就是从页面的0字节开始数,数112个字节);槽0中的值是99,代表最小记录的地址偏移量;
  • 注意最小和最大记录的头信息中的n_owned属性
    • 最小记录的n_owned值为1,这就代表着以最小记录结尾的这个分组中只有1条记录,也就是最小记录本身;
    • 最大记录的n_owned值为5,这就代表着以最大记录结尾的这个分组中只有5条记录,包括最大记录本身还有我们自己插入的4条记录;

用箭头指向的方式替代数字,这样更易于理解,修改后如下:

页目录分组的个数确定?上面示例中为什么最小记录的n_owned值为1,而最大记录的n_owned值为5呢?

InnoDB规定:对于最小记录所在的分组只能有1条记录,最大记录所在的分组拥有的记录条数只能在1~8条之间,剩下的分组中记录的条数范围只能在是 4~8 条之间

分组是按照下边的步骤进行的:

  • 初始情况下一个数据页里只有最小记录和最大记录两条记录,它们分属于两个分组;
  • 之后每插入一条记录,都会从页目录中找到主键值比本记录的主键值大并且差值最小的槽,然后把该槽对应的记录的n_owned值加1,表示本组内又添加了一条记录,直到该组中的记录数等于8个;
  • 在一个组中的记录数等于8个后再插入一条记录时,会将组中的记录拆分成两个组,一个组中4条记录,另一个5条记录,这个过程会在页目录中新增一个槽来记录这个新增分组中最大的那条记录的偏移量;

页目录结构下如何快速查找记录?

如上图所示,比方说查找主键值为6的记录,过程是这样的:

  • 计算中间槽的位置:(0+4)/2=2,所以查看槽2对应记录的主键值为8,又因为8 >6,所以设置high=2,low=0保持不变;
  • 重新计算中间槽的位置:(0+2)/2=1,所以查看槽1对应的主键值为4,又因为4<6,所以设置low=1,high保持不变;
  • 因为high - low的值为1,所以确定主键值为6的记录在槽2对应的组中,此时需要找到槽2中主键值最小的那条记录,然后沿着单向链表遍历槽2中的记录;

Tips:前面说过,每个槽对应的记录都是该组中主键值最大的记录,这里槽2对应的记录是主键值为8的记录,怎么定位一个组中最小的记录呢? 前面说到各个槽都是挨着的(串联的,记录也是按顺序串联的),通过槽1对应的记录(主键值为4)找到该条记录的下一条记录就是槽2中主键值最小的记录,该记录的主键值为5,所以从这条主键值为5的记录出发,遍历槽2中的各条记录,直到找到主键值为6的那条记录即可;

由于一个组中包含的记录条数只能是1~8条,所以遍历一个组中的记录的代价是很小的。

小结:在一个数据页中查找指定主键值的记录的过程分为两步:

  • 通过二分法确定该记录所在的槽,并找到该槽所在分组中主键值最小的那条记录;
  • 通过记录的next_record属性遍历该槽所在的组中的各个记录;

Page Header(页头部)

为了能得到一个数据页中存储的记录的状态信息,比如本页中已经存储了多少条记录,第一条记录的地址是什么,页目录中存储了多少个槽等等,特意在页中定义了一个叫 Page Header 的部分,这个部分占用固定的56个字节,专门存储各种状态信息,详细信息见下表:

名称 占用空间大小 描述
PAGE_N_DIR_SLOTS 2字节 在页目录中的槽数量
PAGE_HEAP_TOP 2字节 还未使用的空间最小地址,也就是说从该地址之后就是Free Space
PAGE_N_HEAP 2字节 本页中的记录的数量(包括最小和最大记录以及标记为删除的记录)
PAGE_FREE 2字节 第一个已经标记为删除的记录的记录地址(各个已删除的记录通过next_record也会组成一个单链
PAGE_GARBAGE 2字节 已删除记录占用的字节数
PAGE_LAST_INSERT 2字节 最后插入记录的位置
PAGE_DIRECTION 2字节 记录插入的方向
PAGE_N_DIRECTION 2字节 一个方向连续插入的记录数量
PAGE_N_RECS 2字节 该页中记录的数量(不包括最小和最大记录以及被标记为删除的记录)
PAGE_MAX_TRX_ID 8字节 修改当前页的最大事务ID,该值仅在二级索引中定义
PAGE_LEVEL 2字节 当前页在B+树中所处的层级
PAGE_INDEX_ID 8字节 索引ID,表示当前页属于哪个索引
PAGE_BTR_SEG_LEAF 10字节 B+树叶子段的头部信息,仅在B+树的Root页定义
PAGE_BTR_SEG_TOP 10字节 B+树非叶子段的头部信息,仅在B+树的Root页定义

PAGE_DIRECTION:假如新插入的一条记录的主键值比上一条记录的主键值大,则这条记录的插入方向是右边,反之则是左边,用来标识最后一条记录插入方向的状态就是PAGE_DIRECTION;

PAGE_DIRECTION:假设连续几次插入新纪录的方向都是一致的,InnoDB会把沿着同一个方向插入记录的条数记下来,这个条数就用PAGE_DIRECTION这个状态标表示,当然,如果最后一条记录的插入方向改变了的话,这个状态的值会被清零重新统计;

7、从数据页的角度看B+树如何查询 一颗B+树按照节点类型可以分成两部分:

  • 叶子节点:B+树最底层的节点,节点的高度为0,存储行记录;
  • 非叶子节点,节点的高度大于0,存储索引键和页面指针,并不存储行记录本身;

当从页结构来理解B+树的结构的时候,可以帮我们理解一些通过索引进行检索的原理:

  • B+树是如何进行记录检索的?

如果通过B+树的索引查询行记录,首先是从B+树的根开始,逐层检索,直到找到叶子节点,也就是找到对应的数据页为止,将数据页加载到内存中,页目录中的槽(slot)采用 二分查找 的方式先找到一个粗略的记录分组,然后再在分组汇总通过链表遍历的方式查找记录。

  • 普通索引和唯一索引在查询效率上有什不同?

创建索引的时候可以是普通索引,也可以是唯一索引,那么这两个索引在查询效率上有什么不同呢?

  • 唯一索引就是在普通索引上增加了约束性,也就是关键字唯一,找到了关键字就停止检索;
  • 普通索引,可能会存在用户记录中的关键字相同的情况,根据页结构的原理,当读取一条记录的时候,不是单独将这条记录从磁盘中读出去,而是将这个记录所在的页加载到内存中进行读取;
  • InnoDB存储引擎的页大小为16KB,在一个页中可能存储着上千条记录,因此在普通索引的字段上进行查找也就是在内存中多几次 判断下一条记录 的操作,对于CPU来说,这些操作所消耗的时间是可以忽略不计的。所以对一个索引字段进行检索,采用普通索引还是唯一索引在检索效率上基本上没有差别;

2.3 InnoDB行格式(记录格式)详解

1、行(记录)简介

在MySQL中,是以 行(记录) 为单位来向表中插入数据,这些 记录 在磁盘上的存放方式也被称为 行格式 或者 记录格式。 InnoDB存储引擎设计了4种不同类型的 行格式,分别是 CompactRedundantDynamiccompressed 行格式。

查看MySQL的默认行格式

1
2
3
4
SELECT @@innodb_default_row_format;

# 也可以使用如下语法查看具体表使用的行格式;
SHOW TABLE STATUS LIKE '表名'\G

指定行格式的语法:

1
2
3
CREATE TABLE table_name (rows info) ROW_FORMAT=rowformat_name;

ALTER TABLE table_name ROW_FORMAT=rowformat_name;

实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
CREATE TABLE record_test_table(
	col1 VARCHAR(8),
  col2 VARCHAR(8) NOT NULL,
  col3 CHAR(8),
  col4 VARCHAR(8)
) CHARSET = ASCII ROW_FORMAT=CPMPACT; # 创建表时指定表的行格式为 Compact

INSERT INTO record_test_table(col1, col2, col3, col4)VALUES('zhangsan', 'lisi', 'wangwu', 'songhk'),('tong', 'chen', NULL, NULL);

ALTER TABLE record_test_table ROW_FORMAT=REDUNDANT; # 修改表指定表的行格式为 REDUNDANT

2、COMPACT行格式详解

在MySQL 5.1版本中,默认设置为Compact行格式。 一条完整的记录可以被分为 记录的额外信息记录的真实数据 两部分, 如下图: 变长字段长度列表

MySQL支持一些变长的数据类型,比如VARCHAR(M)、VARBINARY(M)、TEXT类型,BLOB类型,这些数据类型修饰列称为变长字段,变长字段中存储多少字节的数据不是固定的,所以在存储真实数据的时候需要顺便把这些数据占用的字节数也存起来。

在Compact行格式中,把所有变长字段的真实数据占用的字节长度都存放在记录的开头部位,从而形成一个变长字段长度列表。

注意:这里面存储的变长长度和字段顺序是反过来的。比如两个varchar字段在表结构的顺序是a(10),b(15)。那么在变长字段长度列表中存储的长度顺序就是15,10,是反过来的。

以record_test_table表中的第一条记录举例:因为record_test_table表的col1、col2、col3列都是VARCHAR(8)类型的,所以这三个列的值的长度都需要保存在记录开头处,注意record_test_table表中的各个列都使用的是ascii字符集(每个字符只需要1个字节来进行编码)。 又因为这些长度值需要按照列的逆序存放,所以最后变长字段长度列表的字节串用十六进制表示的效果就是(各个字节之间实际上没有空格,用空格隔开知识方便理解):06 04 08

把这个节点串组成的变长字段长度列表填入上边的示意图中的效果就是: NULL值列表

Compact行格式会把可以为NULL的列统一管理起来,存在一个标记为NULL值列表中。如果表中没有允许存储 NULL 的列,则 NULL值列表也不存在了。

之所以要存储NULL是因为数据都是需要对齐的,如果没有标注出来NULL值的位置,就有可能在查询数据的时候出现混乱。如果使用一个特定的符号放到相应的数据位表示空置的话,虽然能达到效果,但是这样很浪费空间,所以就在行数据得头部开辟出一块空间专门用来记录该行数据哪些是非空数据,哪些是空数据,格式如下:

  • 二进制位的值为1时,代表该列的值为NULL。
  • 二进制位的值为0时,代表该列的值不为NULL。

注意:顺序同样也是反过来存放的

例如: 有字段a、b、c,其中a是主键,在某一行中存储的数依次是a=1、b=null、c=2,那么Compact行格式中的NULL值列表中存储:01。第一个0表示c不为null,第二个1表示b是null。这里之所以没有a是因为数据库会自动跳过主键,因为主键肯定是非NULL且唯一的,在NULL值列表的数据中就会自动跳过主键。

record_test_table的两条记录的NULL值列表就如下:

记录头信息(5字节)

记录头信息长度为 5字节,结构如下图: 这些记录头信息中各个属性如下下表:

名称 大小(bit) 描述
预留位1 1 没有使用
预留位2 1 没有使用
delete_mask 1 标记该记录是否被删除
mini_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned 4 表示当前记录拥有的记录数
heap_no 13 表示当前记录在记录堆的位置信息
record_type 3 表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录
next_record 16 表示下一条记录的相对位置

示例:

1
2
INSERT INTO page_demo VALUES
(1,100,'song'),(2,200,'tong'),(3,300,'zhan'),(4,400,"lisi");

向 空表 page_demo 插入上述四条记录(行)数据后,数据页中User Records 的 head如下图所示:

  • delete_mask:这个属性标记着当前记录是否被删除,占用1个二进制位;
    • 值为0:代表记录并没有被删除
    • 值为1:代表记录被删除掉了

Tips: 被删除的记录为什么还在页中存储呢?

  • 你以为它删除了,可它还在真实的磁盘上。这些被删除的记录之所以不立即从磁盘上移除,是因为移除它们之后其它的记录在磁盘上需要重新排列,导致性能消耗。所以只是打一个删除标记而已,所有被删除掉的记录都会组成一个所谓的 垃圾链表,在这个链表中的记录占用的空间称之为 可重用空间,之后如果有新记录插入到表中的话,可能把这些被删除的记录占用的存储空间覆盖掉。
  • min_rec_mask:B+树的每层非叶子节点中的最小记录都会添加该标记,min_rec_mask值为1。前面我们自己插入的四条记录的min_rec_mask值都是0,意味着它们都不是B+树的非叶子节点中的最小记录;

  • record_type:这个属性表示当前记录的类型,一共有4种类型的记录:

    • 0:表示普通记录
    • 1:表示B+树非叶节点记录
    • 2:表示最小记录
    • 3:表示最大记录
  • heap_no:这个属性表示当前记录在本页中的位置。

Tips: 怎么不见heap_no值为0和1的记录呢 ?

  • MySQL会自动给每个页里加了两个记录,由于这两个记录并不是我们自己插入的,所以有时候也称为伪记录或者虚拟记录。这两个伪记录一个代表最小记录,一个代表最大记录。最小记录和最大记录的heap_no值分别是0和1,也就是说它们的位置最靠前
  • n_owned:页目录中每个组中最后一条记录的头信息中会存储该组一共有多少条记录;

  • next_record:记录头信息里该属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量; 比如, 第一条记录的next_record值为32,意味着从第一条记录的真实数据的地址处向后找32个字节便是下一条记录的真实数据。

Tips: 注意,下一条记录指得并不是按照插入顺序的下一条记录,而是按照主键值由小到大排好序的下一条记录,而且规定 Infimum记录(也就是最小记录)的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)。

下图用箭头代替偏移量表示next_record:

从表中删除一条记录,这个链表也会跟着变化:

1
2
mysql> DELETE FROM page_demo where c1 = 2;
Query OK, 1 row affected(0,02 sec)

删除 page_demo 表的第二行记录后的示意图如下: 从图中可以看出来,删除第2条记录前后主要发生了以下变化:

  • 第2条记录并没有从存储空间中移除,而是把该条记录的delete_mask值设置为1
  • 第2条记录的next_record值变为了0,意味着该记录没有下一条记录了
  • 第1条记录的next_record指向了第3条记录
  • 最大记录的n_owned值从5变成了4

所以,不论怎么对页中的记录做增删改操作,InnoDB始终会维护一条记录的单链表,链表中的各个节点是按照主键值由小到大的顺序连接起来的

记录的真实数据 之隐藏数据列

列名 是否必须 占用空间 描述
row_id 6字节 行ID,唯一标识一条记录
transaction_id 6字节 事务ID
roll_pointer 7字节 回滚指针

实际上这几个列的真正名称是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。

Tips: 一个表没有手动定义主键,MySQL会选取一个Unique键作为主键,如果连Unique键都没有定义的话,MySQL则会为表添加一个名为row_id的隐藏列作为主键,所以row_id是在没有自定义主键以及Unique键的情况下才会存在的。

3、Dynamic和Compressed行格式 行溢出 和 页的扩展* InnoDB存储引擎可以将一条记录中的某些数据存储在真正的数据页面之外。

  • MySQL数据库提供的 VARCHAR(M)类型 可以存放65535字节吗? 如果使用ascii字符集的话,一个字符就代表一个字节,看看VARCHAR(65535)是否可用:
1
2
3
CREATE TABLE varchar_size_demo(
	c VARCHAR(65535)
)CHARSET=ascii ROW_FORMAT=Compact;

结果如下:

ERROR 1118(42000):Row size too large.The maximum row size for the used tabletype, not counting BLOBs, is 65535. This includes storage overhead, check the manual.You have to change some columns tp TEXT or BLOBs

报错信息表达的意思是: MySQL对一条记录占用的最大存储空间是有限制的,除BLOB或者TEXT类型的列之外,其它所有的列(不包括隐藏列和记录头信息)占用的字节长度加起来不能超过65535个字节。

65535个字节 除了列本身的数据之外,还包括一些其它的数据,以Compact行格式为例,比如说为了存储一个VARCHAR(M)类型的列,除了真实数据占有空间以外,还需要记录的额外信息。

如果该VARCHAR类型的列没有NOT NULL属性,那最多只能存储65532个字节的数据,因为变长字段的长度占用2个字节,NULL值标识需要1个字节。

1
2
3
CREATE TABLE varchar_size_demo(
	c VARCHAR(655352)
)CHARSET=ascii ROW_FORMAT=Compact;

如果有not null属性,那么就不需要NULL值标识,也就是可以多存储一个字节,即65533个字节

1
2
3
CREATE TABLE varchar_size_demo(
	c VARCHAR(65533)
)CHARSET=ascii ROW_FORMAT=Compact;

一个页的大小一般是16KB,也就是16384字节,而一个VARCHAR(M)类型的列就最多可以存储65533个字节,这样就可能出现一个页存放不了一条记录,这种现象称为 行溢出.

在Compact和Reduntant行格式中,对于占用存储空间非常大的列,在记录的真实数据处只会存储该列的一部分数据(存放768个前缀字节),把剩余的数据分散存储在几个其它的页中进行 分页存储,然后记录的真实数据处用20个字节存储指向这些页的地址(当然这20个字节中还包括这些分散在其它页面中的数据的占用的字节数),从而可以找到剩余数据所在的页。这称为 页的扩展。如下图所示:

  • Compact和Redundant两种格式会在记录的真实数据处存储一部分数据(存放768个前缀字节);

在MySQL 8.0中,默认行格式就是Dynamic,Dynamic、Compressed行格式和Compact行格式挺像,只不过在处理行溢出数据时有不同:

  • Compressed和Dynamic两种记录格式对于存放在BLOB中的数据采用了完全的行溢出的方式。如下图,在数据页中只存放20个字节的指针(溢出页的地址),实际的数据都存放在Off Page(溢出页)中。

Compressed行记录格式 的另一个功能就是,存储在其中的行数据会以zlib的算法进行压缩,因此对于BLOB、TEXT、VARCHAR这类大长度类型的数据能够进行非常有效的存储。

4、Redundant行格式 Redundant是MySQL 5.0版本之前InnoDB的行记录存储方式,MySQL 5.0支持Redundant是为了兼容之前版本的页格式。

现在把表record_test_table的行格式修改为Redundant:

1
2
3
ALTER TABLE record_test_table ROW_FORMAT=Redundant;
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: o Warnings: 0

从上图可以看到,不同于Compact行记录格式,Redundant行格式的首部是一个字段长度偏移列表,同样是按照列的顺序逆序放置的。

注意Compact行格式的开头是变长字段长度列表,而Redundant行格式的开头是字段长度偏移列表,与变长字段长度列表有两处不同:

  • 少了 变长 两个字: Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表;
  • 多了 偏移 两个字: 这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度;

举例: 比如第一条记录的字段长度偏移列表就是: 2B 25 1F 1B13 0C 06 因为它是逆序排放的,所以按照列的顺序排列就是: 06 0C 1317 1A24 25

按照两个相邻数值的差值来计算各个列值的长度的意思就是:

  • 第一列(row_id)的长度就是Ox06个字节,也就是6个字节。
  • 第二列(transaction_id)的长度就是(0x0C- 0x06)个字节,也就是6个字节。
  • 第三列(roll_pointer)的长度就是(0x13 - 0x0C)个字节,也就是7个字节。
  • 第四列(col1)的长度就是(0x1B - 0x13)个字节,也就是8个字节。
  • 第五列(col2)的长度就是(0x1F - 0x1B)个字节,也就是4个字节。
  • 第六列(col3)的长度就是(0x25 - 0x1F)个字节,也就是6个字节。
  • 第七列(col4)的长度就是(0x2B - 0x25)个字节,也就是6个字节。

不同于Compact行格式,Redundant行格式中的记录头信息固定占用6个字节(48位),每位的含义见下表: 与Compact行格式的记录头信息对比来看,有两处不同:

  • Redundant行格式多了 n_field1byte_offs_flag 这两个属性;
  • Redundant行格式没有 record_type 这个属性;

其中,n_fields: 代表一行中列的数量,占用10位,这也很好地解释了为什么MySQL一个行支持最多的列为1023。另一个 1byte_offs_flags 值定义了偏移列表占用1个字节还是2个字节,当它的值为1时,表明使用1个字节存储,当它的值为0时,表明使用2个字节存储。

2.4 InnoDB数据存储结构 - 区、段和碎片区简介

InnoDB数据存储结构如下图所示:

1、为什么要有区?

B+树的每一层中的页都会形成一个双向链表,如果是 以页为单位 来分配存储空间的话,双向链表相邻的两个页之间的物理位置可能离得非常远。

介绍B+树索引的使用场景的时候特别提到范围查询只需要定位到最左边的记录和最右边的记录,然后沿着双向链表一直扫描就可以了,而如果链表中相邻的两个页物理位置离得非常远,就是所谓的随机I/O。

磁盘的速度和内存的速度差了好几个数量级,随机I/O是非常慢,所以应该尽量让链表中相邻的页的物理位置也相邻,这样进行范围查询的时候才可以使用所谓的 顺序I/O

引入 的概念,一个区就是物理位置上连续的64个页,因为InnoDB中的页的大小默认是16KB,所以一个区的大小是 64*16KB=1MB

在表中数据量大的时候,为某个索引分配空间的时候就不再按照页的单位分配了,而是按照区为单位分配,甚至在表中的数据特别多的时候,可以一次性分配多个连续的区,虽然可能造成一点点空间的浪费(数据不足以填充满整个区),但是从性能角度看,可以消除很多的随机I/O,功大于过!

区的分类: 区大体上可以分为4种类型:

  • 空闲的区(FREE):现在还没有用到这个区中的任何页面。
  • 有剩余空间的碎片区(FREE_FRAG):表示碎片区中还有可用的页面。
  • 没有剩余空间的碎片区(FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面。
  • 附属于某个段的区(FSEG):每一索引都可以分为叶子节点段和非叶子节点段

处于 FREEFREE_FRAG 以及 FULL_FRAG 这三种状态的区都是独立的,直属于表空间。而处于 FSEG 状态的区是附属于某个段的。

2、为什么要有段?

对于范围查询,其实是对B+树叶子节点中的记录进行顺序扫描,而如果不区分叶子节点和非叶子节点,统统把节点代表的页面放到申请到的区中的话,进行范围扫描的效果就大打折扣了。

所以InnoDB对B+树的叶子节点和非叶子节点进行了区别对待,也就是说叶子节点有自己独有的区,非叶子节点也有自己独有的区。存放叶子节点的区的集合就算是一个 段(segment),存放非叶子节点的区的集合也算是一个。也就是说 一个索引会生成2个段,一个叶子节点段,一个非叶子节点段

除了索引的叶子节点段和非叶子节点段之外,InnoDB中还有为存储一些特殊的数据而定义的段,比如 回滚段。所以,常见的段有 数据段索引段回滚段。数据段即为B+树的叶子节点,索引段即为B+树的非叶子节点。

在InnoDB存储引擎中,对段的管理都是由引擎自身所完成,DBA不能也没有必要对其进行控制。这从一定程度上简化了DBA对于段的管理。

段其实不对应表空间中的某一个连续的物理区域,而是一个逻辑上的概念,由若干个零散的页面以及一些完整的区组成

3、为什么要有碎片区?

默认情况下,一个使用InnoDB存储引擎的表只有一个聚簇索引,一个索引会生成2个段,而段是以区为单位申请存储空间的,一个区默认占用1M(64*16KB=1024KB)存储空间;

所以 默认情况下一个只存在几条记录的小表也需要2M的存储空间么?以后每次添加一个索引都要多申请2M的存储空间么?

这对于存储记录比较少的表简直是天大的浪费,这个问题的症结在于到现在为止介绍的区都是非常纯粹的,也就是一个区被整个分配给某一个段,或者说区中的所有页面都是为了存储同一个段的数据而存在的,即使段的数据填不满区中所有的页面,那余下的页面也不能挪作它用。

为了考虑以完整的区为单位分配给某个段对于数据量较小的表太浪费存储空间的这种情况,InnoDB提出了一个 碎片(fragment)区 的概念。

在一个碎片区中,并不是所有的页都是为了存储同一个段的数据而存在的,而是碎片区中的页可以用于不同的目的,比如有些页面用于段A,有些页面用于段B,有些页甚至哪个段都不属于。

碎片区直属于表空间,并不属于任何一个段。

所以此后为某个段分配存储空间的策略是这样的:

  • 在刚开始向表中插入数据的时候,段是从某个碎片区以单个页面为单位来分配存储空间的;
  • 当某个段已经占用了32个碎片区页面之后,就会申请以完整的区为单位来分配存储空间;

所以现在段不能仅定义为是某些区的集合,更精确的应该是 某些零散的页面 以及 一些完整的区 的集合。

2.5 InnoDB数据存储结构 - 表空间

1、表空间简介

表空间 可以看做是InnoDB存储引擎逻辑结构的最高层,所有的数据都存放在表空间中。

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

表空间数据库由一个或多个表空间组成,表空间从管理上可以划分为 系统表空间(System tablespace)独立表空间(File-per-table tablespace)撤销表空间(Undo Tablespace)临时表空间(Temporary TableSpace) 等。

2、独立表空间

独立表空间 即每张表有一个独立的表空间,也就是数据和索引信息都会保存在这个独立的表空间中,独立的表空间(即:单表)可以在不同的数据库之间进行 迁移

空间可以回收(DROP TABLE操作可自动回收表空间;其它情况,表空间不能自己回收)。如果对于统计分析或是日志表,删除大量数据后可以通过: alter table TableName engine=innodb; 回收不用的空间。对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。

独立表空间由段、区、页(前面已经讲解过) 组成。

真实表空间对应的文件大小 在数据库的数据目录里可以看到一个新建的表对应的.ibd文件只占用了96K,才6个页面大小(MySQL5.7中),这是因为一开始表空间占用的空间很小,因为表里边都没有数据,这些.ibd文件是自扩展的,随着表中数据的增多,表空间对应的文件也逐渐增大。

查看InnoDB的表空间类型:

1
show variables like 'innodb_file_per_table';

看到 innodb_file_per_table=ON,这就意味着每张表都会单独保存为一个 .ibd 文件。

3、 系统表空间

系统表空间 的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这部分是独立表空间中没有的。

InnoDB数据字典 每当向一个表中插入一条记录的时候,MySQL校验过程如下:

  • 先要校验一下插入语句对应的表存不存在,插入的列和表中的列是否符合;
  • 如果语法没有问题的话,还需要知道该表的聚簇索引和所有二级索引对应的根页面是哪个表空间的哪个页面;
  • 然后把记录插入对应索引的B+树中;

所以说,MySQL除了保存着插入的用户数据之外,还需要保存许多额外的信息,比方说:

  • 某个表属于哪个表空间,表里边有多少列
  • 表对应的每一个列的类型是什么
  • 该表有多少索引,每个索引对应哪几个字段,该索引对应的根页面在哪个表空间的哪个页面
  • 该表有哪些外键,外键对应哪个表的哪些列
  • 某个表空间对应文什系统上文件路径是什么

上述这些数据并不是我们使用INSERT语句插入的用户数据,实际上是为了更好的管理用户数据而不得已引入的一些额外数据,这些数据也称为 元数据,InnoDB存储引擎特意定义了一系列的 内部系统表(Internal System table) 来记录这些元数据:

表名 描述
SYS_TABLES 整个InnoDB存储引擎中所有的表的信息
SYS_COLUMNS 整个InnoDB存储引擎中所有的列的信息
SYS_INDEXES 整个InnoDB存储引擎中所有的索引的信息
SYS_FIELDS 整个InnoDB存储引擎中所有的索引对应的列的信息
SYS_FOREIGN 整个InnoDB存储引擎中所有的外键的信息
SYS_FOREIGN_COLS 整个InnoDB存储引擎中所有的外键对应列的信息
SYS_TABLESPACES 整个InnoDB存储引擎中所有的表空间信息
SYS_DATAFILES 整个InnoDB存储引擎中所有的表空间对应文件系统的文件路径信息
SYS_VIRTUAL 整个InnoDB存储引擎中所有的虚拟生成列的信息

这些系统表也被称为 数据字典,它们都是以B+树的形式保存在系统表空间的某些页面中,其中SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS这四个表尤其重要,称之为 基本系统表(basic system tables)

注意:用户是不能直接访问InnoDB的这些内部系统表,除非你直接去解析系统表空间对应文件系统上的文件。不过考虑到查看这些表的内容可能有助于分析问题,所以在系统数据库information_schema中提供了一些以innodb_sys开头的表:

1
2
USE information_schema;
SHOW TABLES LIKE 'innodb_sys%';

三、MyISAM引擎:主要的非事务处理存储引擎

MyISAM提供了大量的特性,包括全文索引、压缩、空间函数(GIS)等,但MyISAM不支持事务、行级锁、外键,有一个毫无疑问的缺陷就是崩溃后无法安全恢复。

MySQL5.5版本之前默认的存储引擎

优势是访问的速度快,对事务完整性没有要求或者以SELECT、INSERT为主的应用

针对数据统计有额外的常数存储。故而 count(*) 的查询效率很高

数据文件结构:

  • 表名.frm 存储表结构
  • 表名.MYD 存储数据 (MYData)
  • 表名.MYI 存储索引 (MYIndex)

应用场景:只读应用或者以读为主的业务

四、MyISAM 和 InnoDB 有什么区别

MyISAM和InnoDB的区别:

  • InnoDB支持事务,MyISAM不支持;
  • InnoDB支持外键,而MyISAM不支持;
  • InnoDB是聚集索引,而MyISAM是非聚集索引;
  • Innodb不支持全文索引,而MyISAM支持全文索引;
  • InnoDB支持表、行级锁,而MyISAM支持表级锁;
  • InnoDB表必须有唯一索引,而MMyISAM可以没有;
  • 存储文件不同。

MyISAM 是MySQL 5.5 之前默认存储引擎,InnoDB 是MySQL 5.5 版本之后默认存储引擎。

MyISAM 只有表级锁,InnoDB 支持行级锁和表级锁、默认为行级锁。

MyISAM 不提供事务支持,InnoDB 提供事务支持,实现了 SQL 标准定义的四个隔离级别,具有提交和回滚事务的能力。InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。

MyISAM 不支持外键,InnoDB 支持外键。

MyISAM 不支持数据库异常崩溃后的安全恢复,InnoDB 支持数据库异常崩溃后的安全恢复。使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态,这个恢复的过程依赖于 redo log 。

MyISAM 不支持 MVCC,InnoDB 支持 MVCC。

Tips: MVCC 是 Multi-Version Concurremt Control 的简称,意思是基于多版本的并发控制协议,通过版本号避免同一数据在不同事务间的竞争。它主要是为了提高数据库的并发读写性能,不用加锁就能让多个事务并发读写。

MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。

  • InnoDB 引擎中,其数据文件本身就是索引文件,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶子节点 data 域保存了完整的数据记录;
  • MyISAM 索引文件和数据文件是分离的,索引保存的是数据文件的指针;

InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系。

五、InnoDB 的 redo log 详解

5.1 redo log 简介

redo log(重做日志) 是InnoDB存储引擎独有的,它让MySQL拥有了崩溃恢复能力。

比如 MySQL 实例挂了或宕机了,重启时,InnoDB存储引擎会使用redo log恢复数据,保证数据的持久性与完整性。

MySQL在更新表数据的时候,如果发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。然后会把 在某个数据页上做了什么修改 记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。

5.2 redo log 的刷盘时机

InnoDB 存储引擎为 redo log 的刷盘策略提供了 innodb_flush_log_at_trx_commit 参数,它支持三种策略:

  • 设置为0的时候,表示每次事务提交时 不进行刷盘 操作,只是保留在 redo log buffer中,mysql 崩溃会丢失1s的数据;
  • 设置为1的时候,表示每次事务提交时 都将进行 刷盘操作(默认值),持久化到磁盘;
  • 设置为2的时候,表示每次事务提交时 都只把redo log buffer内容写入page cache,OS宕机会丢失1s的数据,因为未进行持久化;

innodb_flush_log_at_trx_commit 参数默认为 1 ,也就是说当事务提交时会调用 fsync(同步操作) 对 redo log 进行刷盘。

另外 InnoDB 存储引擎 有一个后台线程,每隔1秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。

redo log buffer占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动刷盘。

5.3 redo log 是怎么记录日志的

硬盘上存储的 redo log 日志文件不只一个,而是以一个 日志文件组 的形式出现的,每个redo日志文件大小都是一样的。

比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录4G的内容。

它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示: 所以,如果数据写满了但是还没有来得及将数据真正的刷入磁盘当中,那么就会发生「内存抖动」现象,从肉眼的角度来观察会发现 mysql 会宕机一会儿,此时就是正在刷盘了。

5.4 什么是 undo log

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作(INSERT、DELETE、UPDATE)进行回滚,在 MySQL 中,恢复机制是通过回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。

每次对记录进行改动都会记录一条 undo log,每条 undo log 也都有一个DB_ROLL_PTR属性,可以将这些 undo log 都连起来,串成一个链表,形成版本链,版本链的头节点就是当前记录最新的值。

六、Archive 引擎:用于数据存档

archive 是归档的意思,仅仅支持插入和查询两种功能(行被插入后不能再修改)。

Archive 引擎具有以下特性:

  • 在MySQL5.5以后支持索引功能;
  • 拥有很好的压缩机制,使用zlib压缩库,在记录请求的时候实时的进行压缩,经常被用来作为仓库使用;

    根据英文的测试结论来看,同样数据量下,Archive表比MyISAM表要小约75%,比支持事务处理的InnoDB表小约83%;

  • 创建Archive表时,存储引擎会创建名称以表名开头的文件,数据文件的扩展名为 .ARZ
  • Archive存储引擎采用了行级锁;
  • Archive引擎支持 AUTO_INCRENENT列属性,AUTO_INCREMENT列可以具有唯一索引非唯一索引,尝试在任何其它列上创建索引会导致错误; Archive表适合日志和数据采集(档案)类应用;适合存储大量的独立的作为历史记录的数据,拥有很高的插入速度,但是对查询的支持较差;

下表展示了Archive存储引擎功能:

特征 是否支持
B树索引 不支持
备份/时间点恢复(在服务器中实现,而不是在存储引擎中) 支持
集群数据库支持 不支持
聚集索引 不支持
压缩数据 支持
数据缓存 不支持
加密数据(加密功能在服务器中实现) 支持
外键支持 不支持
全文检索索引 不支持
地理空间数据类型支持 支持
哈希索引 不支持
索引缓存 不支持
锁粒度 行锁
MVCC 不支持
存储限制 没有任何限制
交易 不支持
更新数据字典的统计信息 支持

七、CSV 存储引擎:存储数据时,以逗号分隔各个数据项

CSV存储引擎的特征

  • CSV存储引擎 可以将普通的CSV文件作为MySQL的表来处理,但不支持索引;
  • CSV存储引擎可以作为一种数据交换的机制,非常有用;
  • CSV存储的数据直接可以在操作系统里,用文本编辑器,或者excel读取;
  • CSV存储引擎对于数据的快速导入、导出是有明显优势的;

创建CSV表时,服务器会创建一个纯文本数据文件,其名称以表名开头并带有.CSV扩展名,当你将数据存储到表中时,存储引擎将其以逗号分隔值格式保存到数据文件中。

创建CSV表还会创建相应的元文件,用于存储表的状态和表中存在的行数了。此文件的名称与表的名称相同,后缀为CSM。

CSV表使用案例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
mysql> CREATE TABLE test (i INT NOT NULLc CHAR(10)NOT NULL)ENGINE = CSV;
Query 0K,0 rows affected (0.06 sec)

mysql> INSERT INTO test VALUES( 1 , ' record one ' ),(2,'record two ' ) ;
Query oK2 rows affected (0.05 sec)
Records : 2 Duplicates: 0warnings : 0

mysql> SELECT * FROM test ;
+---+-----------+
| i | c         |
+---+-----------+
| 1 | record one|
| 2 | record two|
+---+-----------+
2rows in set (0.00 sec)

打开CSV表文件如下:

1
2
"1", "record one"
"2", "record two"

八、Memory 引擎:置于内存的表

Memory引擎 采用的逻辑存储介质是内存,响应速度很快,但是当mysqld守护进程崩溃的时候数据会丢失;另外,要求存储的数据是数据长度不变的格式,比如,Blob和Text类型的数据不可用(长度不固定的)。*

Memory引擎主要特征:

  • Memory同时支持哈希(HASH)索引和B+树索引;
    • 哈希索引相等的比较快,但是对于范围的比较慢很多;
    • 默认使用哈希(HASH)索引,其速度要比使用B型树(BTREE)索引快;
    • 如果希望使用B树索引,可以在创建索引时选择使用;
  • Memory表至少比MyISAM表要快一个数量级;
  • MEMORY表的大小是受到限制的,表的大小主要取决于两个参数,分别是 max_rowsmax_heap_table_size, 其中,max_rows 可以在创建表时指定; max_heap_table_size 的大小默认为16MB,可以按需要进行扩大;
  • 数据文件与索引文件分开存储;
    • 每个基于MEMORY存储引擎的表实际对应一个磁盘文件,该文件的文件名与表名相同,类型为frm类型,该文件中只存储表的结构,而其数据文件都是存储在内存中的;
    • 这样有利于数据的快速处理,提供整个表的处理效率;
  • 缺点:其数据易丢失,生命周期短,基于这个缺陷,选择MEMORY存储引擎时需要特别小心;

使用Memory存储引擎的场景:

  • 目标数据比较小,而且非常频繁的进行访问,在内存中存放数据,如果太大的数据会造成内存溢出,可以通过参数 max_heap_table_size 控制Memory表的大小,限制Memory表的最大的大值;
  • 如果数据是临时的,而且必须立即得到,那么就可以放在内存中;
  • 存储在Memory表中的数据如果突然间丢失的话也没有太大的关系;

九、NDB引擎:MySQL集群专用存储引擎

NDB引擎 也叫做NDB Cluster存储引擎,主要用于MysQL cluster 分布式集群环境,类似于Oracle的 RAC集群。

十、其它支持的存储引擎简介

10.1 Federated 引擎:访问远程表

Federated引擎 是访问其它MySQL服务器的一个代理,尽管该引擎看起来提供了一种很好的跨服务器的灵活性,但也经常带来问题,因此默认是禁用的。

10.2 Merge引擎:管理多个MyISAM表构成的表集合

10.3 Blackhole 引擎:丢弃写操作,读操作会返回空内容

Blackhole引擎 没有实现任何存储机制,它会丢弃所有插入的数据,不做任何保存,但服务器会记录Blackhole表的日志,所以可以用于复制数据到备库 或者 简单地记录到日志,但这种应用方式会碰到很多问题,因此并不推荐。

Licensed under CC BY-NC-SA 4.0
最后更新于 2022-11-12 17:51 CST