Bootstrap

[mysql]面试-InnoDB记录存储结构

内容学习自(但并不是完全复制粘贴😁):

https://juejin.cn/book/6844733769996304392/section/6844733770046636040

作者:小孩子4919

目录

指定行格式的语法

Compact格式

变长字段长度列表 

NULL值列表


下面就是一些面试假想,这块复习好了,要引导面试官讲mysql的时候给他讲讲内存页~

面试官:你们mysql用的是什么存储引擎呢?

答:innodb

那么为什么用innodb呢?有什么好处呢?他的存储结构是什么样的呢?

首先,mysql 存储引擎中,想要获取某些记录的时候,并不是一条一条的读出来的,而是将数据划分为若干页。

以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。

MYSQL是行级存储,跟大数据的一些列级分布式存储是有区别的。

行格式有CompactRedundant 美[rɪˈdʌndənt]、DynamicCompressed行格式

指定行格式的语法

我们可以在创建或修改表的语句中指定行格式

CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称
    
ALTER TABLE 表名 ROW_FORMAT=行格式名称

我们数据库一般用的是什么是行格式呢?

一般都是默认的。使用:下面语句,查看到我们线上用的都是Dynamic,那么接下来就得重点关注dynamic了~

USE xxx;
show table status like 'table_name';

Compact格式

看这个图眉清目秀的,直接从小孩子那弄过来了.面试官问,记录长什么样就靠这个了.

一条完整的记录其实可以被分为记录的额外信息记录的真实数据两大部分

变长字段长度列表 

这边其实最关键的信息提取:(快速记忆)

1.变长的字段是有个列表的

2.变长的列表在每一行内容的前面

3.按照我们存储的列,逆序存放

注意看:这个表是这样的,数据是下面那样

我们只把,变长字段存在一个记录的头部。

然后C1 C2 C4这三列,是变长,对应长度是4 3 1

那么存在头部就是,

用十六进制表示的效果就是(各个字节之间实际上没有空格,用空格隔开只是方便理解):

01 03 04 

计算到底是放一个字节还是两个字节:(下面知道个大概就行,面试官不会这么蛋疼问这个吧)

由于第一行记录中c1c2c4列中的字符串都比较短,也就是说内容占用的字节数比较小,用1个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2个字节来表示。具体用1个还是2个字节来表示真实数据占用的字节数,InnoDB有它的一套规则,我们首先声明一下WML的意思:

  1. 假设某个字符集中表示一个字符最多需要使用的字节数为W,也就是使用SHOW CHARSET语句的结果中的Maxlen列,比方说utf8字符集中的W就是3gbk字符集中的W就是2ascii字符集中的W就是1

  2. m是对于变长类型VARCHAR(M)来说,这种类型表示能存储最多M个字符(注意是字符不是字节),所以这个类型能表示的字符串最多占用的字节数就是M×W

  3. 假设它实际存储的字符串占用的字节数是L

  • 如果M×W <= 255,那么使用1个字节来表示真正字符串占用的字节数。

    也就是说InnoDB在读记录的变长字段长度列表时先查看表结构,如果某个变长字段允许存储的最大字节数不大于255时,可以认为只使用1个字节来表示真正字符串占用的字节数。
    
  • 如果M×W > 255,则分为两种情况:

    • 如果L <= 127,则用1个字节来表示真正字符串占用的字节数。

    • 如果L > 127,则用2个字节来表示真正字符串占用的字节数。

    InnoDB在读记录的变长字段长度列表时先查看表结构,如果某个变长字段允许存储的最大字节数大于255时,该怎么区分它正在读的某个字节是一个单独的字段长度还是半个字段长度呢?设计InnoDB的大叔使用该字节的第一个二进制位作为标志位:如果该字节的第一个位为0,那该字节就是一个单独的字段长度(使用一个字节表示不大于127的二进制的第一个位都为0),如果该字节的第一个位为1,那该字节就是半个字段长度。
    
    对于一些占用字节数非常多的字段,比方说某个字段长度大于了16KB,那么如果该记录在单个页面中无法存储时,InnoDB会把一部分数据存放到所谓的溢出页中(我们后边会唠叨),在变长字段长度列表处只存储留在本页面中的长度,所以使用两个字节也可以存放下来。
    

(这边记个todo,溢出页的概念)

总结一下就是说:如果该可变字段允许存储的最大字节数(M×W)超过255字节并且真实存储的字节数(L)超过127字节,则使用2个字节,否则使用1个字节。

 (其实上面的东西有个思想挺好玩的,就是在设计的时候,怎么区分某个字节是一个单独的字段长度还是半个字段长度呢?设计InnoDB的大叔使用该字节的第一个二进制位作为标志位:如果该字节的第一个位为0,那该字节就是一个单独的字段长度(使用一个字节表示不大于127的二进制的第一个位都为0),如果该字节的第一个位为1,那该字节就是半个字段长度。

一个字节能表示多大的数? (一个字节可以表示的最大值127,最小值-128)

(这边还会涉及一个概念 : 原码 反码 补码 )

注意:

1.不是所有的表都有这个头,如果没有一个变长字段就没有这个变长头。

2.如果字段为null,那么不计入这个变长字段列表中。没有就空着。

NULL值列表

我们将允许存储NULL的列,对应一个二进制位,然后按列的顺序逆序排列。

如果一个表中有9个允许为NULL,那这个记录的NULL值列表部分就需要2个字节来表示了。

  • 对于第一条记录来说,c1c3c4这3个列的值都不为NULL,所以它们对应的二进制位都是0,画个图就是这样:

    image_1c9g8m05b19ge1c8v2bf163djre6e.png-21.5kB

  • 所以第一条记录的NULL值列表用十六进制表示就是:0x00

  • 对于第二条记录来说,c1c3c4这3个列中c3c4的值都为NULL,所以这3个列对应的二进制位的情况就是:

    image_1c9g8ps5c1snv1bhj3m48151sfl6r.png-20.6kB

 所以总结:

就是能为NULL的列,会一定存放在这边,然后0就是非NULL,1就是NULL

注意逆序,最终会转成16进制存储.

图中数字指的是16进制.

记录头信息

它是由固定的5个字节组成。5个字节也就是40个二进制位,不同的位代表不同的意思,如图:

image_1c9geiglj1ah31meo80ci8n1eli8f.png-29.5kB

这个感觉更加重要~

这些二进制位代表的详细信息如下表:

名称大小(单位:bit)描述
预留位11没有使用
预留位21没有使用
delete_mask1标记该记录是否被删除
min_rec_mask1B+树的每层非叶子节点中的最小记录都会添加该标记
n_owned4表示当前记录拥有的记录数
heap_no13表示当前记录在记录堆的位置信息
record_type3表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点记录,2表示最小记录,3表示最大记录
next_record16表示下一条记录的相对位置

我们怎么找到下一条记录在哪的,就是通过next_record

delete_mask1标记该记录是否被删除

 其实mysql,删除记录,并不是立马就删除。

在 InnoDB 中,你的 delete 操作,并不会真的把数据删除,mysql 实际上只是给删除的数据打了个标记,标记为删除,因此你使用 delete 删除表中的数据,表文件在磁盘上所占空间不会变小,我们这里暂且称之为假删除

被删除的记录行,只是被标记删除,是可以被复用的,下次有符合条件的记录是可以直接插入到这个被标记的位置的。

比如我们在 id 为 300-600 之间的记录中删除一条 id=500 的记录,这条记录就会被标记为删除,等下一次如果有一条 id=400 的记录要插入进来,那么就可以复用 id=500 被标记删除的位置,这种情况叫行记录复用

数据页复用,就是指整个数据页都被标记删除了,于是这整个数据页都可以被复用了,和行记录复用不同的是,数据页复用对要插入的数据几乎没有条件限制。

每次向磁盘读一次数据就是读一个数据页,然而每访问一个数据页就对应一次磁盘 IO 操作,磁盘 IO 相对内存访问速度是相当慢的。

(MYSQL 怎么进行瘦身?)使用下面这个命令就能解决数据空洞问题。

optimize table t

命令的原理就是重建表,就是建立一个临时表 B,然后把表 A(存在数据空洞的表) 中的所有数据查询出来,接着把数据全部重新插入到临时表 B 中,最后再用临时表 B 替换表 A 即可,这就是重建表的过程。

那么线上允不允许这样做呢?明问问DBA ?(当数据已经有几亿的时候,还可以这样做么,那么我们业务库是怎么进行瘦身的呢?)

记录的真实数据

(这边隐藏列 其实就涉及到了 我们也许没有添加主键,但是这个有主键应该就没没有row_id了)

对于record_format_demo表来说,记录的真实数据除了c1c2c3c4这几个我们自己定义的列的数据以外,MySQL会为每个记录默认的添加一些列(也称为隐藏列),具体的列如下:

列名是否必须占用空间描述
DB_ROW_ID6字节行ID,唯一标识一条记录
DB_TRX_ID6字节事务ID
DB_ROLL_PTR7字节回滚指针

实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR,我们为了美观才写成了row_id、transaction_id和roll_pointer。

这里需要提一下InnoDB表对主键的生成策略(最近业务中经常会用到这个):优先使用用户自定义主键作为主键,如果用户没有定义主键,则选取一个Unique键作为主键,如果表中连Unique键都没有定义的话,则InnoDB会为表默认添加一个名为row_id的隐藏列作为主键。所以我们从上表中可以看出:InnoDB存储引擎会为每条记录都添加 transaction_id 和 roll_pointer 这两个列,但是 row_id 是可选的(在没有自定义主键以及Unique键的情况下才会添加该列)。这些隐藏列的值不用我们操心,InnoDB存储引擎会自己帮我们生成的。

注意:

char类型,会用空格来填充。空格字符在ascii字符集的表示就是0x20

Dynamic和Compressed行格式

MySQL版本是5.7,它的默认行格式就是Dynamic,这俩行格式和Compact行格式挺像,只不过在处理行溢出数据时有点儿分歧,它们不会在记录的真实数据处存储字段真实数据的前768个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址,就像这样:

image_1conbtnmr1sg1hao1nf41pi1eb72a.png-29.9kB

Compressed行格式和Dynamic不同的一点是,Compressed行格式会采用压缩算法对页面进行压缩,以节省空间。

Redundant

主要是偏移量,也不是很常用,就记一下上面那种吧。

  • 字段长度偏移列表

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

    • 没有了变长两个字,意味着Redundant行格式会把该条记录中所有列(包括隐藏列)的长度信息都按照逆序存储到字段长度偏移列表

    • 多了个偏移两个字,这意味着计算列值长度的方式不像Compact行格式那么直观,它是采用两个相邻数值的差值来计算各个列值的长度。

      比如第一条记录的字段长度偏移列表就是:

      25 24 1A 17 13 0C 06
      

      因为它是逆序排放的,所以按照列的顺序排列就是:

      06 0C 13 17 1A 24 25
      

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

      第一列(`row_id`)的长度就是 0x06个字节,也就是6个字节。
      
      第二列(`transaction_id`)的长度就是 (0x0C - 0x06)个字节,也就是6个字节。
      
      第三列(`roll_pointer`)的长度就是 (0x13 - 0x0C)个字节,也就是7个字节。
      
      第四列(`c1`)的长度就是 (0x17 - 0x13)个字节,也就是4个字节。
      
      第五列(`c2`)的长度就是 (0x1A - 0x17)个字节,也就是3个字节。
      
      第六列(`c3`)的长度就是 (0x24 - 0x1A)个字节,也就是10个字节。
      
      第七列(`c4`)的长度就是 (0x25 - 0x24)个字节,也就是1个字节。
;