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官方文档

相关推荐
陈卓4104 分钟前
MySQL-主从复制&分库分表
android·mysql·adb
你都会上树?1 小时前
MySQL MVCC 详解
数据库·mysql
长征coder2 小时前
AWS MySQL 读写分离配置指南
mysql·云计算·aws
ladymorgana2 小时前
【docker】修改 MySQL 密码后 Navicat 仍能用原密码连接
mysql·adb·docker
PanZonghui2 小时前
Centos项目部署之安装数据库MySQL8
linux·后端·mysql
GreatSQL社区3 小时前
用systemd管理GreatSQL服务详解
数据库·mysql·greatsql
掘根3 小时前
【MySQL进阶】错误日志,二进制日志,mysql系统库
数据库·mysql
weixin_438335403 小时前
基础知识:mysql-connector-j依赖
数据库·mysql
小明铭同学3 小时前
MySQL 八股文【持续更新ing】
数据库·mysql
程序员岳焱13 小时前
Java 与 MySQL 性能优化:Java 实现百万数据分批次插入的最佳实践
后端·mysql·性能优化