大家好,我是小林。
之前有位读者在面字节的时候,被问到这么个问题:
如果你知道 MySQL 一行记录的存储结构,那么这个问题对你没什么难度。
如果你不知道也没关系,这次我跟大家聊聊MySQL 一行记录是怎么存储的?
知道了这个之后,除了能应解锁前面这道面试题,你还会解锁这些面试题:
这些问题看似毫不相干,其实都是在围绕「 MySQL 一行记录的存储结构」这一个知识点,所以攻破了这个知识点后,这些问题就引刃而解了。
好了,话不多说,发车!
MySQL 的数据存放在哪个文件?
大家都知道 MySQL 的数据都是保存在磁盘的,那具体是保存在哪个文件呢?
MySQL 存储的行为是由存储引擎实现的,MySQL 支持多种存储引擎,不同的存储引擎保存的文件自然也不同。
InnoDB 是我们常用的存储引擎,也是 MySQL 默认的存储引擎。所以,本文主要以 InnoDB 存储引擎展开讨论。
先来看看 MySQL 数据库的文件存放在哪个目录?
mysql SHOW VARIABLES Variable_name Value>|varlibmysql row sec
我们每创建一个>
然后,我们进入 /var/lib/mysql/my_test 目录,看看里面有什么文件?
root@xiaolin #ls varlibmysqlmy_testdbt_ordert_order

可以看到,共有三个文件,这三个文件分别代表着:
db.opt,用来存储当前数据库的默认字符集和字符校验规则。
t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
t_order.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以存放在独占表空间文件(文件名:表名字.idb)。这个行为是由参数 innodb_file_per_table 控制的,若设置了参数 innodb_file_per_table 为 1,则会将存储的数据、索引等信息单独存储在一个独占表空间,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了,因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .idb 文件。
好了,现在我们知道了一张数据库表的数据是保存在「 表名字.idb 」的文件里的,这个文件也称为独占表空间文件。
那这个表空间文件的结构是怎么样的?
表空间由段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑存储结构大致如下图:
下面我们从下往上一个个看看。
1、行(row)
数据库表中的记录都是按行(row)进行存放的,每行记录根据不同的行格式,有不同的存储结构。
后面我们详细介绍 InnoDB 存储引擎的行格式,也是本文重点介绍的内容。
2、页(page)
记录是按照行来存储的,但是数据库的读取并不以「行」为单位,否则一次读取(也就是一次 I/O 操作)只能处理一行数据,效率会非常低。
因此,InnoDB 的数据是按「页」为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个行记录从磁盘读出来,而是以页为单位,将其整体读入内存。
默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。
页是 InnoDB 存储引擎磁盘管理的最小单元,意味着数据库每次读写都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。
页的类型有很多,常见的有数据页、undo 日志页、溢出页等等。数据表中的行记录是用「数据页」来管理的,数据页的结构这里我就不讲细说了,之前文章有说过,感兴趣的可以去看这篇文章:换一个角度看 B+ 树
总之知道表中的记录存储在「数据页」里面就行。
3、区(extent)
我们知道 InnoDB 存储引擎是用 B+ 树来组织数据的。
B+ 树中每一层都是通过双向链表连接起来的,如果是以页为单位来分配存储空间,那么链表中相邻的两个页之间的物理位置并不是连续的,可能离得非常远,那么磁盘查询时就会有大量的随机I/O,随机 I/O 是非常慢的。
解决这个问题也很简单,就是让链表中相邻的页的物理位置也相邻,这样就可以使用顺序 I/O 了,那么在范围查询(扫描叶子节点)的时候性能就会很高。
那具体怎么解决呢?
在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
4、段(segment)
表空间是由各个段(segment)组成的,段是由多个区(extent)组成的。段一般分为数据段、索引段和回滚段等。
索引段:存放 B + 树的非叶子节点的区的集合;
数据段:存放 B + 树的叶子节点的区的集合;
回滚段:存放的是回滚数据的区的集合,之前讲事务隔离的时候就介绍到了 MVCC 利用了回滚段实现了多版本查询数据。
好了,终于说完表空间的结构了。接下来,就具体讲一下 InnoDB 的行格式了。
之所以要绕一大圈才讲行记录的格式,主要是想让大家知道行记录是存储在哪个文件,以及行记录在这个表空间文件中的哪个区域,有一个从上往下切入的视角,这样理解起来不会觉得很抽象。
InnoDB 行格式有哪些?
行格式(row_format),就是一条记录的存储结构。
InnoDB 提供了 4 种行格式,分别是 Redundant、Compact、Dynamic和 Compressed 行格式。
Redundant 行格式我这里就不讲了,因为现在基本没人用了,这次重点介绍 Compact 行格式,因为 Dynamic 和 Compressed 这两个行格式跟 Compact 非常像。
所以,弄懂了 Compact 行格式,之后你们在去了解其他行格式,很快也能看懂。
COMPACT 行格式长什么样?
先跟 Compact 行格式混个脸熟,它长这样:
可以看到,一条完整的记录分为「记录的额外信息」和「记录的真实数据」两个部分。
接下里,分别详细说下。
记录的额外信息
记录的额外信息包含 3 个部分:变长字段长度列表、NULL 值列表、记录头信息。
1. 变长字段长度列表
varchar(n) 和 char(n) 的区别是什么,相信大家都非常清楚,char 是定长的,varchar 是变长的,变长字段实际存储的数据的长度(大小)不固定的。
所以,在存储数据的时候要把这些数据占用的字节数也存起来,存到「变长字段长度列表」里面,读取数据的时候才能根据这个「变长字段长度列表」去读取对应长度的数据。其他 TEXT、BLOB 等变长字段也是这么实现的。
为了展示「变长字段长度列表」具体是怎么保存变长字段占用的字节数,我们先创建这样一张表,字符集是 ascii(所以每一个字符占用的 1 字节),行格式是 Compact,t_user 表中 name 和 phone 字段都是变长字段:
`t_user` `id` `name` `phone` DEFAULT `age` DEFAULT PRIMARY KEY `id` USING BTREE ENGINE InnoDB DEFAULT CHARACTER ascii ROW_FORMAT COMPACT
现在 t_user 表里有这三条记录:
接下来,我们看看看看这三条记录的行格式中的 「变长字段长度列表」是怎样存储的。
先来看第一条记录:
这些变长字段的长度值会按照列的顺序逆序存放(等下会说为什么要这么设计),所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。
同样的道理,我们也可以得出第二条记录的行格式中,「变长字段长度列表」里的内容是「 04 02」,如下图:
第三条记录中 phone 列的值是 NULL,NULL 是不会存放在行格式中记录的真实数据部分里的,所以「变长字段长度列表」里不需要保存值为 NULL 的变长字段的长度。
为什么「变长字段长度列表」的信息要按照逆序存放?
这个设计是有想法的,主要是因为「记录头信息」中指向下一个记录的指针,指向的是下一条记录的「记录头信息」和「真是数据」之间的位置,这样的好处是向左读就是记录头信息,向右读就是真实数据,比较方便。
「变长字段长度列表」中的信息之所以要逆序存放,是因为这样可以使得位置靠前的记录的真实数据和数据对应的字段长度信息可以同时在一个 CPU Cache Line 中,这样就可以提高 CPU Cache 的命中率。
同样的道理, NULL 值列表的信息也需要逆序存放。
如果你不知道什么是 CPU Cache,可以看这篇文章:面试官:如何写出让 CPU 跑得更快的代码?,这属于计算机组成的知识。
每个数据库表的行格式都有「变长字段字节数列表」吗?
其实变长字段字节数列表不是必须的。
当数据表没有变长字段的时候,比如全部都是 int 类型的字段,这时候表里的行格式就不会有「变长字段长度列表」了,因为没必要,不如去掉以节省空间。
所以「变长字段长度列表」只出现在数据表有变长字段的时候。
2. NULL 值列表
表中的某些列可能会存储 NULL 值,如果把这些 NULL 值都放到记录的真实数据中会比较浪费空间,所以 Compact 行格式把这些值为 NULL 的列存储到 NULL值列表中。
如果存在允许 NULL 值的列,则每个列对应一个二进制位(bit),二进制位按照列的顺序逆序排列。
另外,NULL 值列表必须用整数个字节的位表示(1字节8位),如果使用的二进制位个数不足整数个字节,则在字节的高位补0。
还是以 t_user 表的这三条记录作为例子:
接下来,我们看看看看这三条记录的行格式中的 NULL 值列表是怎样存储的。
先来看第一条记录,第一条记录所有列都有值,不存在 NULL 值,所以用二进制来表示是酱紫的:
但是 InnoDB 是用整数字节的二进制位来表示NULL值列表的,现在不足 8 位,所以要在高位补 0,最终用二进制来表示是酱紫的:
所以,对于第一条数据,NULL 值列表用十六进制表示是 0x00。
接下来看第二条记录,第二条记录 age 列是 NULL 值,所以,对于第二条数据,NULL值列表用十六进制表示是 0x04。
最后第三条记录,第三条记录 phone 列 和 age 列是 NULL 值,所以,对于第三条数据,NULL 值列表用十六进制表示是 0x06。
我们把三条记录的 NULL 值列表都填充完毕后,它们的行格式是这样的:
每个数据库表的行格式都有「NULL 值列表」吗?
NULL 值列表也不是必须的。
当数据表的字段都定义成 NOT NULL 的时候,这时候表里的行格式就不会有 NULL 值列表了。所以在设计数据库表的时候,通常都是建议将字段设置为 NOT NULL,这样可以节省 1 字节的空间(NULL 值列表占用 1 字节空间)。
3. 记录头信息
记录头信息中包含的内容很多,我就不一一列举了,这里说几个比较重要的:
记录的真实数据
记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer,我们来看下这三个字段是什么。
如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。
事务id,表示这个数据是由哪个事务生成的。trx_id是必需的,占用 6 个字节。
这条记录上一个版本的指针。roll_pointer 是必需的,占用 7 个字节。
如果你熟悉 MVCC 机制,你应该就清楚 trx_id 和 roll_pointer 的作用了,如果你还不知道 MVCC 机制,可以看完这篇文章,一定要掌握,面试也很经常问 MVCC 是怎么实现的。
varchar(n) 中 n 最大取值为多少?
varchar(n) 字段类型的 n 代表的是最多存储的字符数量,那 n 最大能设置多少?
这个问题要考虑两个因素:
行格式中「变长字段长度列表」有时候是占用 1 字节,有时候是占用 2 字节:
可以看到,「变长字段长度列表」占用的字节数最大不会不超过 2 字节。
2 个字节的最大值是 65535(十进制),从这里可以推测一行记录最大能存储 65535 字节的数据,实际上真的是这样吗?
我这里以 ascii 字符集作为例子,这意味着 1 个字符占用 1 字节。那么 varchar(65535) 就意味着最多可存储 65535 个 ascii 字符,刚好满足一行记录最大能存储 65535 字节的数据。
我们定义一个 varchar(65535) 类型的字段,字符集为 ascii 的数据库表。
test `name` ENGINE InnoDB DEFAULT CHARACTER ascii ROW_FORMAT COMPACT
看能不能成功创建一张表:
可以看到,创建失败了。
从报错信息就可以知道一行数据的最大字节数是 65535(不包含 TEXT、BLOBs 这种大对象类型),其中包含了 storage overhead。
问题来了,这个 storage overhead 是什么呢?其实就是变长字段长度列表和 NULL 值列表,也就是说一行数据的最大字节数 65535,其实是包含「变长字段长度列表」和 「NULL 值列表」所占用的字节数的。
我们存储字段类型为 varchar(n) 的数据时,其实分成了三个部分来存储:
前面我创建表的时候,字段是允许为 NULL 的,所以会占用 1 字节来存储 NULL 标识,字段是变长字段且变长字段允许存储的最大字节数大于 255 字节 ,所以会占用 2 字节存储真实数据的占用的字节数,所以最多可以存储 65535- 2 – 1 = 65532 个字节。
我们先来测试看看 varchar(65533) 是否可行?
可以看到,还是不行,接下来看看 varchar(65532) 是否可行?
可以看到,创建成功了。
当然,我上面这个例子是针对字符集为 ascii 情况,如果采用的是 UTF-8,varchar(n) 最多能存储的数据计算方式就不一样了:
上面所说的只是针对于一个字段的计算方式。
如果有多个字段的话,要保证所有字段的长度 + 变长字段字节数列表所占用的字节数 + NULL值列表所占用的字节数 <= 65535。
行溢出后,MySQL 是怎么处理的?
MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般是16KB,也就是16384字节,而一个 varchar(n) 类型的列最多可以存储65532字节,一些大对象如 TEXT、BLOB 可能存储更多的数据,这时一个页可能就存不了一条记录。这个时候就会发生行溢出,多的数据就会存到另外的「溢出页」中。
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。在一般情况下,InnoDB 的数据都是存放在 「数据页」中。但是当发生行溢出时,溢出的数据会存放到「溢出页」中。
当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。大致如下图所示。
上面这个是 Compact 行格式在发生行溢出后的处理。
Compressed 和 Dynamic 这两个行格式和 Compact 非常类似,主要的区别在于处理行溢出数据时有些区别。
这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中,看起来就像下面这样:
总结
MySQL 的 NULL 值是怎么存放的?
MySQL 的 Compact 行格式中会用「NULL值列表」来标记值为 NULL 的列,NULL 值并不会存储在行格式中的真实数据部分。
NULL值列表会占用 1 字节空间,当表中所有字段都定义成 NOT NULL,行格式中就不会有 NULL值列表,这样可节省 1 字节的空间。
MySQL 怎么知道 varchar(n) 实际占用数据的大小?
MySQL 的 Compact 行格式中会用「变长字段长度列表」存储变长字段实际占用的数据大小。
varchar(n) 中 n 最大取值为多少?
一行记录最大能存储 65535 字节的数据,但是这个是包含「变长字段字节数列表所占用的字节数」和「NULL值列表所占用的字节数」。
如果一张表只有一个 varchar(n) 字段,且允许为 NULL,字符集为 ascii。varchar(n) 中 n 最大取值为 65532。
计算公式:65535 – 变长字段字节数列表所占用的字节数 – NULL值列表所占用的字节数 = 65535 – 2 – 1 = 65532
行溢出后,MySQL 是怎么处理的?
如果一个数据页存不了一条记录,InnoDB 存储引擎会自动将溢出的数据存放到「溢出页」中。
Compact 行格式针对行溢出的处理是这样的:当发生行溢出时,在记录的真实数据处只会保存该列的一部分数据,而把剩余的数据放在「溢出页」中,然后真实数据处用 20 字节存储指向溢出页的地址,从而可以找到剩余数据所在的页。
Compressed 和 Dynamic 这两种格式采用完全的行溢出方式,记录的真实数据处不会存储该列的一部分数据,只存储 20 个字节的指针来指向溢出页。而实际的数据都存储在溢出页中。
参考资料:
什么是sql注入?
SQL是Structured Quevy Language(结构化查询语言)的缩写。 SQL是专为数据库而建立的操作命令集,是一种功能齐全的数据库语言。 在使用它时,只需要发出“做什么”的命令,“怎么做”是不用使用者考虑的。 SQL功能强大、简单易学、使用方便,已经成为了数据库操作的基础,并且现在几乎所有的数据库均支持SQL。 ##1 二、SQL数据库数据体系结构 SQL数据库的数据体系结构基本上是三级结构,但使用术语与传统关系模型术语不同。 在SQL中,关系模式(模式)称为“基本表”(base table);存储模式(内模式)称为“存储文件”(stored file);子模式(外模式)称为“视图”(view);元组称为“行”(row);属性称为“列”(column)。 名称对称如^a^: ##1 三、SQL语言的组成 在正式学习SQL语言之前,首先让我们对SQL语言有一个基本认识,介绍一下SQL语言的组成: 1.一个SQL数据库是表(Table)的集合,它由一个或多个SQL模式定义。 2.一个SQL表由行集构成,一行是列的序列(集合),每列与行对应一个数据项。 3.一个表或者是一个基本表或者是一个视图。 基本表是实际存储在数据库的表,而视图是由若干基本表或其他视图构成的表的定义。 4.一个基本表可以跨一个或多个存储文件,一个存储文件也可存放一个或多个基本表。 每个存储文件与外部存储上一个物理文件对应。 5.用户可以用SQL语句对视图和基本表进行查询等操作。 在用户角度来看,视图和基本表是一样的,没有区别,都是关系(表格)。 用户可以是应用程序,也可以是终端用户。 SQL语句可嵌入在宿主语言的程序中使用,宿主语言有FORTRAN,COBOL,PASCAL,PL/I,C和Ada语言等。 SQL用户也能作为独立的用户接口,供交互环境下的终端用户使用。 ##1 四、对数据库进行操作 SQL包括了所有对数据库的操作,主要是由4个部分组成: 1.数据定义:这一部分又称为“SQL DDL”,定义数据库的逻辑结构,包括定义数据库、基本表、视图和索引4部分。 2.数据操纵:这一部分又称为“SQL DML”,其中包括数据查询和数据更新两大类操作,其中数据更新又包括插入、删除和更新三种操作。 3.数据控制:对用户访问数据的控制有基本表和视图的授权、完整性规则的描述,事务控制语句等。 4.嵌入式SQL语言的使用规定:规定SQL语句在宿主语言的程序中使用的规则。 下面我们将分别介绍: ##2 (一)数据定义 SQL数据定义功能包括定义数据库、基本表、索引和视图。 首先,让我们了解一下SQL所提供的基本数据类型:(如^b^) 1.数据库的建立与删除 (1)建立数据库:数据库是一个包括了多个基本表的数据集,其语句格式为: CREATE DATABASE 〔其它参数〕 其中,在系统中必须是唯一的,不能重复,不然将导致数据存取失误。 〔其它参数〕因具体数据库实现系统不同而异。 例:要建立项目管理数据库(xmmanage),其语句应为: CREATE DATABASE xmmanage (2) 数据库的删除:将数据库及其全部内容从系统中删除。 其语句格式为:DROP DATABASE 例:删除项目管理数据库(xmmanage),其语句应为: DROP DATABASE xmmanage 2.基本表的定义及变更 本身独立存在的表称为基本表,在SQL语言中一个关系唯一对应一个基本表。 基本表的定义指建立基本关系模式,而变更则是指对数据库中已存在的基本表进行删除与修改
关于SQL 数据库的数据类型问题
两个字符型字段分别定义为char(10)和varchar(10),当给它们存入“123”这个数据时,char(10)字段占用十个字节的存储空间,而varchar(10)只占用3个字节存储空间,这就是char和varchar的区别。 可以看出varchar比较适合存储长度变化很大的数据。 nchar和char,nvarchar和varchar的区别在于是否使用Unicode进行编码。 一般情况下在仅仅处理中文及英文,不涉及特殊符号时不需要使用Unicode。 另一种需要用Unicode的情况是需要将字符串数据添加到SQL语句中执行,又不想里面的东西如单引号使SQL产生误解,可以将其用Unicode编码,这时每个字符都将占用两个字节,单引号也不会被SQL识别了。 ntext和text的区别也是一样。 由于每个字符都占用两个字节,比较适合存储纯中文包括少量英文的数据。 smallint、int和bigint的区别仅仅在于位数不同。 smallint可存储2字节整数(-~),int可存储4字节整数(-~),bigint可存储8字节整数(-~)。 smalldatetime用两个字节存储,可表示从1900年1月1日到2079年6月6日之间的任何时间,精确到分钟。 datetime用四个字节存储,可表示1753年1月1日到9999年12月31日的任何时间,精确到百分之三秒。 sql中没有bigdatetime类型。
oracle的分页处理,oracle中针对一个一千条记录的表如果要查200到300的记录怎么查
Oracle有3种分页处理语句1、根据ROWID分页2、按分析函数分页3、按rownum分页其中1的效率最高,2的效率最低,3的效率比2好很多,比1的差距也很小,是经常使用的分页处理语句;3的语句有固定的格式,基本有以下步骤构成a、查询原表,从原表中取出分页中需要的字段,并排序select ename ,sal from emp order by salb、对a取到的内容进行rownum编号select a1.*,rownum rn from (select ename ,sal from emp order by sal) a1 c、添加分页结束行号select a1.*,rownum rn from (select ename ,sal from emp order by sal) a1whererownum<=300d、添加分页开始行号select a2.* from (select a1.*,rownum rn from (select ename ,sal from emp order by sal) a1whererownum<=300) a2 where rn>=200d中的语句可以用作rownum分页的模板使用,使用时修改select ename ,sal from emp order by sal,开始行号,结束行号就可以了。
发表评论