MySQL-InnoDB数据存储结构
磁盘与内存交互基本单位:页
页的大小默认为16kB,数据库与磁盘进行数据交互的基本单位就是页,不管是读一行数据还是多行数据都是将这些行所在的页加载到内存中进行读取
页结构概述
每个页之间在内存上并不相联,只是通过维护双向链表进行逻辑相联,数据页中数据会按照主键值大小排序,组成一个单向链表
页的大小
不同的数据库管理系统(简称DBMS)的页大小不同。比如在 MySQL 的 InnoDB 存储引擎中,默认页的大小是
16KB
,SQL Server 中页的大小为8KB
,而在 Oracle 中我们用术语 "块
" (Block)来表示 "页",Oracle 支持的快大小为2KB, 4KB, 8KB, 16KB, 32KB 和 64KB。 我们可以通过下面的命令来进行查看:
show variables like '%innodb_page_size%'
页的内部结构
页如果按类型划分的话,常见的有 数据页(保存B+树节点)、系统表、Undo 页 和 事务数据页
等。数据页是我们最常使用的页。
数据页的
16KB
大小的存储空间被划分为七个部分,分别是文件头(File Header)、页头(Page Header)、最大最小记录(Infimum + supremum)、用户记录(User Records)、空闲空间(Free Space)、页目录(Page Directory)和文件尾(File Tailer)。
页结构的示意图如下所示:
如下表所示:
从数据库页的角度看B+树如何查询
一颗B+树按照字节类型可以分为两部分:
叶子节点,B+ 树最底层的节点,节点的高度为0,存储行记录。
非叶子节点,节点的高度大于0,存储索引键和页面指针,并不存储行记录本身。
B+树是如何进行记录检索的?
逐层往下找,直到找到对应的数据页(叶子节点),即B+树有几层就要几次磁盘IO,在数据页内部对记录进行大致分组,然后遍历链表查找记录。
普通索引和唯一索引在查询效率上有什么不同?
二者的区别就是,唯一索引找到了符合条件的记录就会停止检索,但普通索引会存在相同关键字的情况,mysql读取记录是按照页为基本单位,一个页中有上千条记录,相同关键字的会存在同一个页中,所以普通索引无非是在内存中进行检索所有记录,这点计算量所耗费的时间对于cpu而言是可以忽略不记得,所以二者的检索效率基本没有差别
区、段与碎片区
为什么要有区?
B+树的每一层的页都会形成一个双向链表,如果单纯以页为单位,双向链表连接的页物理位置可能连接的非常远,扫描下一个页的时候,如果位置离得很远,就会产生随机IO,随机IO是非常慢的,这时候我们也要让链表中逻辑相邻的页,在内存中也尽量相邻,所以就引入区这个概念,一个区有64个页,当表数据量过大时,分配空间就以区为单位来分配,虽然会造成空间浪费,但能消除很多随机IO,大大提高查询效率。
为什么要有段?
为了提高范围查询的效率,对叶子节点和非叶子节点进行分开存储,各自都有属于自己的区,这样子进行范围查询扫描时,只需要去扫描叶子节点的区集合即可,大大提高查询效率。这些区集合就算是一个段,存储叶子节点的为数据段,非叶子节点为索引段(目录项),段是逻辑概念,并非实际的物理内存区域。
为什么要有碎片区?
一个索引会产生两个段,段是区集合,索引一个表如果建立索引会占用2M的存储空间,如果是一个几条记录的小表,就显得很浪费内存空间,此时就引入了碎片区的概念,即在碎片区中所有的页并不是为一个表存在的,在区中的有些页可能单独会被多张表使用,碎片就直属于表空间,并不属于任何一个段。
段分配存储空间的策略:
- 刚开始向表中插入数据时,段是先向碎片区以单个页面为单位来分配存储空间的
- 当某个段占用了32个页面之后,就会脱离碎片区,申请完整的区来分配存储空间
区的分类
区大体上可以分为4种类型:
空闲的区 (FREE) : 现在还没有用到这个区中的任何页面。
有剩余空间的碎片区 (FREE_FRAG):表示碎片区中还有可用的页面。
没有剩余空间的碎片区 (FULL_FRAG):表示碎片区中的所有页面都被使用,没有空闲页面。
附属于某个段的区 (FSEG):每一个索引都可以分为叶子节点段和非叶子节点段。
处于FREE、FREE_FRAG 以及 FULL_FRAG 这三种状态的区都是独立的,直属于表空间。而处于 FSEG 状态的区是附属于某个段的。
表空间
独立表空间
独立表空间,即每张表有一个独立的表空间,也就是数据和索引信息都会保存在自己的表空间中。独立的表空间 (即:单表) 可以在不同的数据库之间进行 迁移
。
空间可以回收 (DROP TABLE 操作可自动回收表空间;其他情况,表空间不能自己回收) 。如果对于统计分析或是日志表,删除大量数据后可以通过:alter table TableName engine=innodb; 回收不用的空间。对于使用独立表空间的表,不管怎么删除,表空间的碎片不会太严重的影响性能,而且还有机会处理。
独立表空间结构
独立表空间由段、区、页组成。
真实表空间对应的文件大小
我们到数据目录里看,会发现一个新建的表对应的 .ibd 文件只占用了 96K,才6个页面大小 (MySQL5.7中),这是因为一开始表空间占用的空间很小,因为表里边都没有数据。不过别忘了这些 .ibd 文件是自扩展的,随着表中数据的增多,表空间对应的文件也逐渐增大。
查看 InnoDB 的表空间类型:
能看到 innodb_file_per_table=ON, 这就意味着每张表都会单词保存一个 .ibd 文件。
show variables like 'innodb_file_per_table'
系统表空间
系统表空间的结构和独立表空间基本类似,只不过由于整个MySQL进程只有一个系统表空间,在系统表空间中会额外记录一些有关整个系统信息的页面,这部分是独立表空间中没有的。
InnoDB数据字典
删除这些数据并不是我们使用 INSERT 语句插入的用户数据,实际上是为了更好的管理我们这些用户数据而不得以引入的一些额外数据,这些数据页称为 元数据。InnoDB 存储引擎特意定义了一些列的 内部系统表 (internal system table) 来记录这些元数据:
这些系统表也称为 数据字典
,它们都是以 B+ 树的形式保存在系统表空间的某个页面中。其中 SYS_TABLES、SYS_COLUMNS、SYS_INDEXES、SYS_FIELDS
这四个表尤其重要,称之为基本系统表 (basic system tables) ,我们先看看这4个表的结构:
数据页加载的三种方式
InnoDB从磁盘中读取数据
最小单位
是数据页。而你想得到的 id = xxx 的数据,就是这个数据页众多行中的一行。对于MySQL存放的数据,逻辑概念上我们称之为表,在磁盘等物理层面而言是按数据页
形式进行存放的,当其加载到 MySQL 中我们称之为缓存页
。
如果缓冲池没有该页数据,那么缓冲池有以下三种读取数据的方式,每种方式的读取速率是不同的:
内存读取
如果该数据存在于内存中,基本上执行时间在 1ms 左右,效率还是很高的。
随机读取
顺序读取