MySQL的varchar水真的太深了——InnoDB记录存储结构
Posted 砖业洋__
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL的varchar水真的太深了——InnoDB记录存储结构相关的知识,希望对你有一定的参考价值。
此篇讲解
varchar
存储原理,知识难度较大且涉及到计算,欢迎有兴趣者阅读。
文章目录
1. InnoDB是干嘛的?
InnoDB
是一个将表中的数据存储到磁盘上的存储引擎。
2. InnoDB是如何读写数据的?
InnoDB
处理数据的过程是发生在内存中的,需要把磁盘中的数据加载到内存中,如果是处理写入或修改请求的话,还需要把内存中的内容刷新到磁盘上。
读写磁盘的速度非常慢,和内存读写差了几个数量级,所以当我们想从表中获取某些记录时,InnoDB
存储引擎将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB
中页的大小默认为 16 KB
。也就是在一般情况下,一次最少从磁盘中读取16KB
的内容到内存中,或者一次最少把内存中的16KB
内容刷新到磁盘中。
注意:innodb_page_size
变量在服务器运行过程中不可以更改,只能在第一次初始化mysql
数据目录时指定。所以页在运行时的大小不可更改。
3. varchar疑问千千万——InnoDB行格式
看到这里,你一定有着和我相同的疑问,比如
varchar(255)
后面这个最大长度应该怎么选择呢?为什么不能varchar(65535)
而最大只能varchar(16383)
呢?我来带你看!
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。行格式有4种,分别是Dynamic
、Compact
、Redundant
和Compressed
MySQL 5+
默认行格式都是Dynamic
, 在MySQL 5
和 MySQL 8
经过验证确实是的。
SHOW VARIABLES LIKE "innodb_default_row_format"
大家在业务中和平时使用中都几乎没有修改过或者注意过InnoDB
行格式,那么我就只重点讲默认行格式dynamic
,让大家更深层次理解平时开发中的varchar
。
请记住这个表结构,后面会围绕这个来讲
CREATE TABLE test (
c1 VARCHAR(10),
c2 VARCHAR(10) NOT NULL,
c3 CHAR(10),
c4 VARCHAR(10)) CHARSET = utf8mb4;
现在业务数据库字符集都是utf8mb4
,我就以这个来讲,把理解难度降到最低。
INSERT INTO test ( c1, c2, c3, c4 )
VALUES('aaaa', '你好啊', 'cc', 'd'),('eeee', 'fff', NULL, NULL);
现在,表中的记录就是这样
3.1 dynamic——innodb默认行格式
关于记录的额外信息这部分,是服务器为了描述这条记录而不得不额外添加的一些信息,这些额外信息分为3
类,分别是变长字段长度列表、NULL
值列表和记录头信息。
在这里我只讲变长字段长度列表、NULL
值列表。因为记录头信息非常的绕和本篇没多大关系。
3.2 innodb怎么知道varchar真正有多长?——变长字段长度列表
一些变长的数据类型,比如VARCHAR(M)
、各种TEXT
类型,各种BLOB
类型,变长数据类型的字段中存储多少字节的数据是不固定的,在存储真实数据的时候需要把这些数据占用的字节数也存起来。
这些变长字段(比如
varchar
)占用的存储空间分为两部分:
- 真正的数据内容部分,放在对应的列
- 真实占用的字节数,放在变长字段列表部分
我们拿test
表中的第一条记录来举个例子。因为test表的c1
、c2
、c4
列都是VARCHAR(10)
类型的,说明最大10
个字符,所以这三个列的值的长度都需要保存在记录开头处,因为test
表中的各个列都使用的是utf8mb4
字符集,每个字符最大需要4
个字节来进行编码(不使用utf8
而是utf8mb4
是因为可能存储emoji
表情,如果只是文字,utf8
就足够),来看一下第一条记录各变长字段内容的长度:
列名 | 存储内容 | 内容长度(十进制表示) | 内容长度(十六进制表示) |
---|---|---|---|
c1 | ‘aaaa’ | 4 字节 | 0x04 |
c2 | ‘你好啊’ | 9 字节 | 0x09 |
c4 | ‘d’ | 1 字节 | 0x01 |
怎么确定这些字段有多少字节?
比如这里c2
的"你好啊",使用如下sql
可以确定
SELECT LENGTH(c2) from test where c1='aaaa';
各变长字段数据占用的字节数按照列的顺序逆序存放!!
由于第一行记录中c1
、c2
、c4
列中的字符串都比较短,也就是说内容占用的字节数比较小,用1
个字节就可以表示,但是如果变长列的内容占用的字节数比较多,可能就需要用2
个字节来表示。到底varchar
能放多少个字符呢?继续往下看。
3.3 varchar(M) 能存多少个字符,为什么提示最大16383?
首先要理解varchar(M)
的M
是说字符个数,而不是字节。
为什么不能varchar(20000)
之类的,是20000
个字符放不下吗?
为什么提示只能最大16383
个字符呢?这个数字是怎么算出来的?
这个我就得和你好好唠嗑了!
varchar
是变长的,varchar(64)
我也可能只存2
个字符,并不是存了64
个字符,谁知道这个类型到底存了几个字符呢?innodb
设计的时候,就已经考虑到了,不过是用字节作为单位,innodb
必须记录变长字段真实占用的字节数L
。当然也不能太长,因为innodb
最多使用2
个字节的空间去记录这个L
。
InnoDB
有它的一套规则,我们引入W
、M
和L
这几个符号:
- 假设某个字符集中最多需要
W
字节来表示一个字符
utf8mb4
字符集中的W
就是4
utf8
字符集中W
就是3
gbk
字符集中的W
就是2
ascii
字符集中的W
就是1
。
- 对于变长类型
VARCHAR(M)
来说,这种类型表示能存储最多M
个字符(注意是字符不是字节)
所以这个类型能表示的字符串最多占用的字节数就是M × W
。- 假设它实际存储的字符串占用的字节数是
L
。
来看极限边界情况,innodb
为了记录一下varchar
真实存储多少个字节,最多提供2
个字节的空间去记录,2
个字节16
个比特位,全部为1
,最大能记录的数字是2^16-1
是65535
个,innodb
最大能记录varchar
占用的字节数就是65535
个,utf8mb4
字符集一个字符是4
个字节,65535 / 4 = 16383.75
,只要varchar
字符数不超过16383
个innodb
就可以记录真实占用的长度,再多就记录不了了!所以就能解释刚刚的图了,我这里再贴一次,varchar(20000)
不行,最大也就16383
个字符
下面说明一下规则(讲解中字符集用utf8mb4
,W=4
)
规则一:如果允许存储的最大字节数M × W <= 255
,即varchar(M)最大字符数M <= 63
时,innodb
只使用1
个字节来表示varchar
占用的真实字节数。
InnoDB
在读记录的变长字段长度列表时先查看表结构,如果某个变长字段允许存储的最大字节数不大于255
时,即字符数不大于63
时,可以只用1
个字节来表示真实数据占用的字节。
规则二:如果允许存储的最大字节数M × W > 255
,即varchar(M)最大字符数M > 63
时,则分为两种情况:
如果实际存储字节L <= 127
,即实际存储字符 <= ⌊127 / 4⌋ = 31 个,innodb
仅仅使用1
个字节就能表示varchar
占用的真实字节数。(⌊ … ⌋表示向下取整)
如果实际存储字节L > 127
,即实际存储字符 > ⌊127 / 4⌋= 31 个,innodb
使用2
个字节来表示varchar
占用的真实字节数。
另外需要注意的是,变长字段列表只存储非NULL
的列的长度。
表记录是这样的
对于第二条记录,c4
列值为NULL
,所以只存储c1
和c2
列即可。
第一条记录的变长字段长度列表部分占用3
字节空间,第二条记录变长字段长度列表部分占用2
字节。
当然,并不是所有记录都有这个变长字段长度列表部分,比方说表中所有的列都不是变长的数据类型或者 所有列的值都是NULL
的话,这一部分就不需要有。实际业务开发中,几乎没有不使用varchar
的,所以实际开发中的记录都会有变长字段长度列表部分
3.4 记录为NULL,innodb如何处理?——NULL值列表
能仔细看到这里,你肯定是个高手了。如果你和我一样开发规范中不推荐NULL
,一般都写NOT NULL
,其实记录中就不存在NULL
值列表了,也节省了空间。
如果表中的某些列可能存储NULL
值,把这些NULL
值都放到记录的真实数据中存储会很占地方,所以dynamic
行格式把这些值为NULL
的列统一管理起来,存储到NULL
值列表中,它的处理过程是这样的:
-
统计表中允许存储
NULL
的列有哪些。
主键列、被NOT NULL
修饰的列都是不可以存储NULL
值的,所以在统计的时候不会把这些列算进去。比方说表test
的3
个列c1、c3、c4
都是允许存储NULL
值的,而c2
列是被NOT NULL
修饰,不允许存储NULL
值。 -
如果表中没有允许存储
NULL
的列,则NULL
值列表也不存在了,否则将每个允许存储NULL
的列对应一个二进制位,二进制位按照列的顺序逆序排列。二进制位的值为1
时,代表该列的值为NULL
,为0
时,代表该列的值不为NULL
。因为表test
的c1、c3、c4
都是允许存储NULL
值的允许为NULL
的列,所以这3
个列和二进制位的对应关系就是这样:
-
NULL
值列表必须用整数个字节的位表示,如果使用的二进制位个数不是整数个字节,则在字节的高位补0
。
以此类推,如果表中有9
个字段都允许为NULL
,那么这个记录的NULL
值列表就需要2
个字节来表示。
对于第一条记录,c1
、c3
、c4
都不为NULL
,对应的为进制位为0
,十六进制表示就是0x00
对于第二条记录,c3
、c4
都是NULL
,对应的二进制位为1
,十六进制表示就是0x06
这两条记录在填充了NULL
值列表后示意图如下:
3.5 某个列数据占用的字节数非常多怎么办?——dynamic行格式的溢出列
如果某个列中存储的数据占用的字节数非常多,该列就可能称为溢出列。
对于占用存储空间非常多的列,在记录真实数据时,该列只会用20
字节空间,而这20
字节的空间不存储数据,因为数据都分散存储在其他几个页中了。这20
字节的空间存储的是分散的页的地址和占用的字节数。分散的页是单链表连接的结构。
后续:如果大家对
innodb
存储结构其他行格式感兴趣,或者我没说的记录头信息,可以去阅读《MySQL是怎样运行的》一书,我和书中不同的是,书中讲的Compact
格式,字符集是ascii
,我选用的是平时开发中用到的默认dynamic
格式,字符集是utf8mb4
,字符集变化后所有的数据我在文中和图中都有重新计算。大家平时或许没关注过行格式,那么就是按照dynamic
格式理解就可以,更贴近实际开发。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------
以上是关于MySQL的varchar水真的太深了——InnoDB记录存储结构的主要内容,如果未能解决你的问题,请参考以下文章
MySQL的varchar水真的太深了——InnoDB记录存储结构