MySQL知识的小透明 - InnoDB行数据是如何存储的

官方文档是这样定义行格式的:表的行格式决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。

MySQL数据放在哪

我们知道,MySQL架构大致分为Server层、存储引擎层,server层负责与客户端的连接、SQL语句的解析、SQL的成本计算和优化等;存储引擎层有很多实现,我们最常用的时InnoDB引擎,其他的还有MyISAMySQL、NDB、Memory、Archive、Federated、Maria等存储引擎

以linux为例,当我们建立一个数据库时,会在/var/lib/mysql目录下,新建一个以数据库命名的目录,用来存储数据库的表结构信息、表数据、还有字符串和比较规则

  • db.opt文件,用于存储数据库默认的字符集和校验规则
  • {表名}.frm,用来存储数据库中的表结构,记录表有哪些字段,分别是什么格式的
  • {表名}.idb,用来存储表的数据, 文件也称为独立表空间

表数据可以存在共享表空间(ibdata1),可以存在独占的表空间,由参数innodb_file_per_table控制,默认时1,每张表的数据都独立放置于一个.idb文件中中

表空间结构

行(Row)

首先,InnoDB是按行进行数据存储的,最原子的存储结构就是行数据,行数据的存储结构我们后面展开。

如何指定行格式?

ini 复制代码
CREATE TABLE {表名} 
(列信息)
ROW_FORMAT={行格式};

页(page)

因为数据最终存储在磁盘中,不可能每次批量读写都针对行数据进行多次磁盘IO,同时我们的很多查询,都是范围查询,所以需要满足空间局部性原理

空间局部性原理:空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问

行数据,都存储在页中,每页的默认大小为16kb,还可以调整为4kb、8kb、32kb、64kb,以页的方式进行磁盘交互,同时行数据存储在连续的页空间中,读写效率比较高

页分为多种类型,存储数据的数据页、用户数据回滚到的undo log页等,B+树索引也用页做数据的记录

我们知道,聚簇索引B+树叶子结点用于存储数据,通过双向链表进行连接,非叶子结点存储索引数据,每个节点内部通过单向链表进行数据连接,叶子结点层双向链表便于进行数据范围查询,不用再从头结点向下进行查询,减少了IO次数。思考一下下面这个问题

B+树非叶子结点间,是否需要双向链表进行连接?

常见的B+树索引图,通常不关注非叶子结点之间的连接,非叶子结点间,也是通过双向链表进行连接的

篇幅有限,长话短说,下载源码,在storage/innobase/btr目录,找到btr0btr.cc,查看btr_create创建索引方法,其中有如下两行,为节点创建prev指针和next指针,并没有区分是否为叶子结点

scss 复制代码
 btr_page_set_next(page, page_zip, FIL_NULL, mtr);
 btr_page_set_prev(page, page_zip, FIL_NULL, mtr);

非叶子结点间双向链表的作用,体现在MySQL的成本计算中

假如有一张user表,在age和score列分别建了索引,面对如下SQL,选age列的索引还是score列的索引,MySQL应该如何选择?

sql 复制代码
SELECT * FROM user WHERE age > 10 and age < 50 and score > 60 and score < 90;

MySQL的成本计算,会基于CPU、IO成本,判断是否使用索引,使用哪个索引,通过非叶子结点之间的双向链表,仅通过非叶子层的遍历,就可以判断使用本索引大概需要搜索多少个列、匹配的数据有多少个

回到上面的例子,假如age索引涉及的数据在非叶子结点一个页中,且数量不多,而socre索引中满足条件的数据跨2个页,基本可以得出结论:age索引,数据少,在非叶子结点占用页少,涉及到的叶子结点一定少,IO成本低,走age索引效率更高

区(extent)

InnoDB使用B+树来管理数据,我们知道页是一块连续的存储空间,存储页中的行数据,节点(页)之间通过指针进行相互连接 如果进行跨页的范围查询,就涉及到随机IO,而随机IO的效率很低

InnoDB为了提升IO效率,在数据量大时,就不按页分配存储空间了,而是按【区】分配。每个区1MB,也是64个默认页大小的空间,连续的64个页被划为同一个区。这使得在双向链表中连续的多个页,在磁盘存储中也相邻,跨页的查询,很大一部分变为顺序IO。

区存储固定64个页,页默认大小变 -> 区大小变

ex: 页32KB -> 区2MB

段(segment)

【段】由一个或多个分区组成,在默认的表空间配置(每个表数据单独存入.idb文件)中,段分为两类:

  • 数据段,表中所有数据都属于数据段
  • 索引段,表中的每个索引,属于自己的段

行数据是如何存储的

InnoDB通过MVCC(多版本并发控制)通过控制生成Read View的时机实现不同隔离级别,我们知道每行中有2个一定有的隐藏字段

  • 事务id(trx_id),标识本条数据,由哪个事务生成
  • 回滚指针(roll_ptr) ,记录上个版本的指针
  • 如果未定义主键,还有有一个主键隐藏字段row_id

InnoDB提供4种行格式:

  • Redundant
    • 非紧凑行格式,5.0以前使用,几乎没人用了,这个不管
  • Compact
    • 紧凑的行格式,相较于Redundant有20%的存储效率提升,5.1~5.6的默认行格式,下面的行格式,基于Compact展开
  • Dynamic和Compressed
    • 基于Compact做了改进,5.7版本后,默认使用Dynamic

对于行数据存储来说,上面2个隐藏字段和表中的其他数据,统归于【真实数据】,行记录之外,还有【记录的额外信息】

变长字段长度列表

在MYSQL中,我们把VARCHAR、VHARBINARY、TEXT、BLOB这几种不确定数据长度的类型,统称为变长字段。【变长字段长度列表】用来存储本行中各变长字段的实际长度,真实的记录还是记在真实数据区的,每个可变长度列的实际长度使用1到2个字节,具体视列的最大长度而定。

【变长字段长度列表】存储位置在行记录的开头部位,各变长记录占用的字节数,逆序存放

举个简单例子

比如有一张user表,

sql 复制代码
CREATE TABLE user (
  `id` int(11) NOT NULL,
  `name` VARCHAR(20) DEFAULT NULL,
  `phone` VARCHAR(20) DEFAULT NULL
) ROW_FORMAT=COMPACT;

name、phone都为varchar类型,有如下几条记录

id name phone
1 a 11
2 b 123

id为1的数据,看一下在变长字段列表中的存储,id不是变长字段,所以略过

  • name值为a,占用1个字节,16进制表示为0x01
  • phone值为11,占用2字节,16进制表示为0x02

在变长字段长度列表中,逆序存放 (为什么逆序存放,放在记录头信息后面说)

NULL值列表

大部分的博客中,为了便于理解,都将NULL值列表单独作为一个字段来讲,但实际上,官方文档中,NULL值列表只是归属于变长字段列表的一个指针,指针初始大小为1字节。

某些列不设置非空时,可以为空,如果将对应列中填入一个NULL值,假如NULL占用1字节的空间,如果某行有很多个NULL列,也会占用相对多的空间。

NULL值列表这部分,使用类似Bitmap的思想,每个列是否为NULL,使用一个bit表示

  • 0表示不为NULL,
  • 1表示为NULL

NULL值列表这部分只能使用整数个字节(8bit/字节)的位表示,如果列数不是8的倍数,那么高位使用0补齐 同样的,NULL值列表的存储,也是逆序的

记录头信息

头信息包括:

  • delete_mask,标识数据是否被删除
    • MySQL删除一条数据,不会直接去磁盘里面把记录删掉,通过一种软删除手段(脏页)进行删除,所以需要标记某行数据是否已经真正被删掉了
  • next_record,记录下一行记录的位置
    • 在数据页中,记录间通过单链表进行连接,这里的next_record就是维护这层单链表结构的指针
  • record_type,当前记录的类型
    • 类型包含:0普通记录,1非叶子结点记录,2最小记录,3最大记录
    • 最小记录、最大记录,用于标识单链表的头尾指针,上一个页的最大记录,指向当前页的最小记录;当前页的最大记录,指向下一个页的最小记录
  • ......

为什么是变长字段列表、NULL值列表都是逆序的?

下面这张图应该可以做解释了,进行行的遍历时,行指针(next_record)放在头信息部分,位置处于额外信息和真实数据中间

当获取某个字段的真实长度或获取是否为空时,只需要从next_record位置分别向前、前后顺序遍历即可找全字段的 额外信息&真实数据

页溢出是怎么回事

TEXT、BLOB数据类型,可以视为不限制存储长度,VARCHAR类型最多存储65535字节,65535字节≈60kb,数据页默认16kb,VARCHAR拉满的情况下,存储一个VARCHAR数据甚至要几个页,一个页存不下,溢出到另外的页一起存储,这就是页溢出

InnoDB是如何处理页溢出的?

COMPACT行格式,将可变字段(VARCHAR、VARBINARY、BLOB、TEXT)的前768个字段,存储在页中,剩余的存到溢出页中,使用20字节大小的指针指向对应页地址

Dynamic行格式,和Compact的区别为Dynamic在当前行中不存数据,仅存储数据指针

如何为我所用

我们大概可以了解到几点:

  1. InnoDB针对每一行记录的存储,分成【真实数据】和【额外信息】两部分
  2. 对于NULL列、变长列,在【额外信息】中存储是否NULL、真实长度,在【真实数据】中存储实际数据,两部分数据合起来才是对应字段才完整
  3. 当前默认使用的行格式为Dynamic,变长字段存储可能产生页溢出(超过768字节),实际存储在其他页中
  4. 真实页的存储形式,会按区进行存储,相邻的多个页存储在相邻的磁盘空间中,以顺序IO优化随机IO

针对这几点,个人感觉可以转化为日常需要注意的:

  1. 对于字符串额存储,长度可以确定(如身份证号等)的话,使用定长类型做定义,避免使用额外的页存储动态长度数据
  2. 变长数据的存储,控制好长度,超过768字节(比如varchar(255),使用utf8mb4,很可能出现)会产生页溢出,实际数据存到其他页,带来更多的IO次数

其他可能需要关注的

  • varchar类型,trim删除头尾空格后再存储
    • varchar类型,尾部空格也会被记入动态长度,如果恰好处于1~2个字段标识可变长度的临界区,可能会浪费更多的存储空间
  • 固定长度的字段,也需要注意存储长度
    • 大于等于768字节的固定长度 列或可变长度列,可以存储在页外,存储实际数据时,对于定义大长度字段需要更谨慎,越多的大长度字段 -> 越多的磁盘IO次数 -> 查询效率越低,可以考虑分别存储,比如部分数据存入文件存储,用id建立关联
  • 字段长度的存储长度,需要结合字符集来看
    • 使用utfmb3,utfmb4这类可变长度字符集,会进行尾部空格的修剪,char(n)意为最多存储n个字符,如果使用utfmb3,定义char(255),此时最多占用765个字节,不会产生页外存储;如果使用utfmb4,若实际存储长度超过768,就会产生页外存储

参考:

MySQL 是怎样运行的:从根儿上理解 MySQL 掘金小册(强烈推荐读一下)
小林coding-# MySQL 一行记录是怎么存储的?
MySQL官方文档

相关推荐
wdxylb1 分钟前
MySQL数据库用户权限控制的实现方法
数据库·mysql·oracle
Green小光1 小时前
MySQL基础篇 - 事务
数据库·mysql
KYumii2 小时前
MySQL-索引
数据库·mysql
无妄啊______3 小时前
mysql笔记10(高级部分--跟数据库管理有关)
数据库·笔记·mysql
写bug如流水11 小时前
【Python】使用 Pydantic + SQLAlchemy + MySQL 实现自动记录创建时间和更新时间
开发语言·python·mysql
wangyue413 小时前
MYSQL 乐观锁
数据库·mysql
Satan71214 小时前
【MySQL】SQL介绍+基础+DDL+数据备份+还原
数据库·sql·mysql
huisheng_qaq16 小时前
【redis-05】redis保证和mysql数据一致性
数据库·redis·mysql·分布式锁·延迟双删·数据一致性
程序员大金17 小时前
基于SpringBoot+Vue+MySQL的旅游网站
javascript·vue.js·spring boot·后端·mysql·intellij-idea·旅游
落霞与孤鹭齐飞。。18 小时前
报刊订阅系统小程序的设计
java·spring boot·mysql·毕业设计·课程设计