我们都知道mysql最后的数据信息最后都是以文件的形式进行存储的,但是他的存储结构是怎么样的呢?
因为数据最后都是以文件形式存储在磁盘中,表由表结构、数据存放。table_name.ibd为存放数据的文件、table_name.frm为表结构。还有一个就是db.opt这个就是我们创建数据库的设置的默认字符集和字符教研规则都会存放在这个文件。
简单来说就是.ibd为表数据存放的地方。.frm为表结构存放的地方。.opt就是当前数据的信息(字符集、校验规则)
空间表:行(row) --> 页(page) --〉 区(extent) --> 段( segment)
- 行(row):数据库中的表数据是按行(row)进行存放,不同的引擎有不同的存储结构的。
- 页(page):记录是按照行来进行存储的。页是innodb最小的存储单位,每一个页大小为16KB,如果需要从磁盘中获取数据的时候,是以页为单位去获取,当然存储也是一样。
- 区(extent):用来管理页(page)的,一个区的大小为1M,因为每一个页的大小为16KB,相当于一个区可以放64个页,而且是连续的,这样对磁盘I/O会有好很多。
- 段(segment):段是由多个区组成的,多个段组成空间表。
大致的一个空间表结构就是这样。
看看row的详细格式吧,其实innodb的存储格式有,就以(cpmpact)来讲吧
compact:
[记录头信息] +[NULL标志位] +[变长字段长度信息] +[字段数据] +[trx_id] +[roll_ptr]+[row_id]
以上是大致一个结构。
[记录头信息]:
- delete_marsk:删除记录的标志,因为在数据库底层来讲删除并不是马上直接删除的,只是将表delete_marsk标识为1.
- next_record:指向下一条记录的指针地址。
- record_type:0(普通用户记录)1(B+树非叶子几点的记录)2(最小记录)3(最大记录)
- 大致就这些
[NULL标志位]:
- 为什么要设计NULL值列表呢?不为NULL单独分配空间、快速判断某个字段是否为NULL、如果为NULL无需存储该字段
- 因为我们在设计表的时候,有些字段可以为null如果null存放在真实的存储空间中会造成空间浪费,所以就将他存储在一个NULL的表中。通常情况下占用1字节,如果null列数超过8的话那就按照下面公式去计算。
- NULL 标志位中用字节数 = ceil(可为NULL列数/8) 向上取整
- 假设数据表为:
CREATE TABLE tests (
id INT NOT NULL,
a INT,
b INT,
c INT,
d INT
);
# 1 demo
INSERT INFO tests(id,a,b,c,d)VALUES(1,NULL,NULL,NULL,NULL)
# 2 demo
INSERT INFO tests(id,a,b,c,d)VALUES(2,2,NULL,NULL,NULL)
# 3 demo
INSERT INFO tests(id,a,b,c,d)VALUES(3,NULL,NULL,NULL,3)
这个时候我们知道abcd四个都是可以为null,所以NULL占用1字节也就是8位(00000000)。
- 二进制1标识,为NULL
- 二进制0标识,不为NULL
# demo 1 标识为
000001111
# demo 2 标识为
00001110
# demo 3 标识为
00000111
demo中可以看出
bit0 a
bit1 b
bit2 c
bit3 d
INSERT INFO tests(id,a,b,c,d)VALUES(3,NULL,NULL,NULL,3)
这个时候 a b c 都是NULL 只有d的值为3
所以对应的标识位
0000 d c b a
0000 0 1 1 1
-------------
0000 0 1 1 1
这个就是NULL标志位的计算方式
[变长字段长度信息]:
要知道有些数据类型长度是不固定的,char为定长,varchar为变长就演变成为存储的长度是不固定的。我们去读取数据的时候要知道数据的长度、起始位置,这个时候就要存储变长字段的长度信息,也是一个列表。
- 为什么要设计变长字段信息?我们在设计数据表的时候如果设定了变长类型的字段,长度是不固定的。因为在去取数据的时候要知道字段长度取到数据。
- 这里如果当前字段 大于 255 占用2字节。小于255占用1字节
CREATE TABLE t (
id INT,
a VARCHAR(10),
b VARCHAR(20),
c VARCHAR(30)
) ROW_FORMAT=COMPACT;
demo 1
INSERT INTO t (id,a,b,c,d) VALUES (1, 'A', 'BB', 'CCC');
demo 2
INSERT INTO t (id,a,b,c,d) VALUES (1, 'A', NULL, 'CCC');
demo 3
INSERT INTO t (id,a,b,c,d) VALUES (1, NULL, NULL, 'CCC');
demo1
[0x03][0x02][0x01]
demo 2
[0x03][0x01]
demo3
[0x03]
其实和NULL列表差不多 都是逆序存放c b a 去存放
[trx_id]:
事务id,由哪个事务生成的。必须字段,占6字节。最后修改该该行的事务ID
[roll_ptr]:
回滚指针。当前记录上一个版本的指针,必须字段,占用7字节
[row_id]:
如果在创建表的时候没有定义主键,row_id将会显式的出现定义主键(确保数据的唯一性)。如果在创建表的时候已经定义好了主键,row_id将会隐式的出现。
如果当前row大于65535的情况下,那么将会一页是存不下这么多数据的。这个时候就会将多出的数据存放在溢出页中,当前数据会用一个溢出页指针指向就可以了。
每一行的格式存储结构大概就是这样了。
看看page的结构。之前说过就是row组成的page。
大致页的结构就是这样
[文件头 - file header]:
page no唯一标识当前页在空间表中的位置。还有两个指针,一个指向下一页的一个指向上一页,这样就可以构成一个双向链表,这样查找效率会很高。
[页头 - page header]:
记录当前页的各种元信息,如记录数量、记录链表的位置。
[最小和最大记录 - (infmum+supermen)]:
其实这两个参数世纪了在page header中的,用于记录当前页的最大值和最小值。
[用户记录 - user records]:
数据页中实际保存的记录。存储内容就是之前我们说的row。
[空闲空间 - free space]:
当前页中未使用的空间。
[页目录 - page directory]:
存储用户记录的相对位置,对记录起到索引的作用
[文件尾 - file tailer]:
检验页是否完整
看看数据页是页目录和用户记录吧。
- 数据页中会将数据进行分组,这个分组主要看数据大小毕竟一页的大小只有16kb。
- 这个时候一页中如果多个分组,那么就需要将每组的地址记录起来,那页目录就是用来记录这些组的地址。
- 如果有X分组那就有X个槽,组成起来就是页目录
展开说说:
- 我们的数据在存储过程中都是有序的这个无可厚非。
- 页目录中的槽用来记录每一组最后一条记录也就是当前分组中的最大记录(数据是有序的)的地址偏移量。
- 在每一组的最后一条记录中行(row)有一个字段会记录当前分组有多少条记录,n_woend字段记录。
- 然后每一组中的的数据就是一条条的行(row)使用指针关联起来。
- 之前说过X分组就有X槽对应,每个槽相当于指针指向自己管理的分组中最后一个记录(row)
分组情况:
- 页中的第一个分组只能有一条记录
- 最后一个分组只能在1-8之间
- 中间部分为4-8之间
总结:
mysql的数据存储结构,宏观上来看就是每一条记录是一行一行的数据,多行数据组组成一个页,一个一个页组成一个个区,一个一个区组层一个段。这个就是大致的结构。其中每页的大小固定16KB。其中row的数据接口大致有投记录信息、变长字段信息、NUll标志位、事务ID(trx_id)、指向旧版本指针(roll_prt)、以及row_id等。然后是页的结构,一个页的大小是固定的,能存放多少数据要取决于数据的大小。在数据页中有文件头信息(file header)、页头信息(page header)、记录也中的最大值和最小值(为记录)、用户记录(真正记录数据的区域)、空闲空间(尚未被使用的空间)、页目录、文件尾等信息。因为我们的数据是一行行的(row)在一页一页中会划分区域进行存放,就是将一页划分多个区去数据。那多个区如何怎么管理,这个时候就需要页目录区管理,页目录主要就是一个个槽(slot)组成的,每个槽管理自己的分组,每个槽会指向当前分组的最大值(数据是有序的),这个分组中的最大值这条记录会使用一个字段去记录当前分组的数据数量,数据和数据之间使用指针串联起来,建立起关联性。