学习资料:
MySQL是怎样运行的:从根儿上理解MySQL
0. 前言
先说一下常见的字符集:
- ascii码,范围就是0-127,用一个字节表示
- gbk编码,中文,1-2个字节
- utf-8,1-3个字节
不同的字符集,其比较规则也不一样。
1. Innodb是如何将表中数据存储到磁盘中的引擎
以页作为磁盘和内存之间交互的基本单位
数据是存在磁盘上的,但是处理数据是在内存中进行的,innodb采用了页这一形式,作为磁盘和内存之间数据交换的基本单位,一页的大小是16kb,也就是说,该存储引擎进行一次数据处理,最少的16kb。
2. 行格式
在innodb引擎,一共有4种行格式
create|alter table xxx row_format=行格式名称
注:
一行数据(所有列)的长度 < 65535个字节。
2.1 Compact格式
所以说,包括记录的额外信息和真实信息两方面
额外记录信息
-
变长字段长度列表
Mysql将变长字段(如varchar)类型的字段分为两个部分,一个是真实数据内容,另外一个是实际占用的字节数。 -
null值列表
-
记录头信息
由5个字节,也就是40个二进制位组成,分别代表不同的标志位,大概如下:
记录真实数据
- 三个隐藏列
事务id(transaction_id)
回滚id(roll_pointer)
row_id,不一定会有,(无主键,无unique列时) - 数据列
注:
当定长字段的字符集是变长的时候(例如utf-8,gbk)其字段长度也会加入变长字段长度列表;
varchar(M)的字段,代表其最多能存储M个字符,字符集最多需要使用的字节数为W,所以代表这个字段最多能占用的字节数为 = M * W;
2.2 Redundant格式
额外信息
-
字段长度偏移列表
记录的不是字段所占的实际字节数,而是从第一个字段开始,每个字段的偏移量,比如一行中,第一列的地址为01,第二列占4个字节,那么第二列的偏移量就是(01 + 4)= 05。
该长度信息是用一个字节存储,还是用2个字节存储,要看记录的真实数据占用的总大小来决定的。当不大于127时,就用1个字节,当大于127,小于32767时,用两个字节。 -
记录头信息
注:
一个字节是8位,也就是256,为啥用127来作为分界点呢?看起来,正好差了8位的最高一位,最高一位,嗯,这么说是不是有点感觉了,对的,标志位。因为Redundant格式没有专门的null值记录,这个最高位就是用来标记该字段是不是null值的。
真实数据
和Compact一样
2.3 行溢出
上面提了一句,一行的真实数据不要超过65535,但其实很好超过的,那么怎么办?
那就和链表一样,分开存储,innodb以页位单位,存储到别的页上面。
在Compact和Redundant格式中,是存储一部分数据(768字节),然后花20字节存储指向溢出页的地址。
但是在Dynamic和Compress格式中,就不存储数据,直接存储溢出页的地址。
在mysql 5.7中,默认行格式就是Dynamic。
3. 数据页(索引页)结构
上面说了,innodb是以页作为磁盘和内存中间数据交换的基本单位,每个页16kb,其实为了不同的目的,innodb设计了很多种不同的页,当然,最基本的还是存实际数据的页。其结构如下:
先说说User Records
顾名思义,这就是我们存记录的地方。
其实在最开始没有存记录时,是没有这个地方的,只有Free Space,就是空的地方,在存入记录时,就从free space开辟空间。
前面说了行格式,在每行记录的头信息中,有一位为 next_record,指向下一条记录的真实数据地址处,就是用这个形式,将一行行记录连接成一个单链表形式。
但是有一个问题,在插入第一条记录的时候,是从哪指过来的呢?嗯,这就是Infimum的作用了,这是默认的最小行,可以看成头结点。同理,supremum最大行就是单链表的尾结点,这条链表是以主键按照从小到大的顺序连起来的。
最大行和最小行的概念,可以简单理解为,按照主键默认从小到大排序的大小。前面说了,如果没有主键(或unique列),会自动产生一个row_id列,就可以简单的理解为以row_id的大小比较。
既然为单链表,那么插入和删除就好办了,本来默认是
infimum -> p -> supremum
在记录p后面插入一条记录s的话,只要将其中next_record的指针改一下就行了。
infimum -> p -> s -> supremum
删除同理,移动链表即可,但是在头信息中有一个标注 delete_mask,标记是否删除。实际上,在逻辑上删除一行,实际物理存储并没有删除,至少没有马上删除,可以留给后面的插入记录进行空间覆盖,或者删除的记录连起来形成一个垃圾链表。
其实mysql中规定了,一页中最少要有2条数据记录,在空着的时候,有infimum + supremum两条,在不断插入的过程中,不断占用free space 的地盘,在记录头信息中的heap_no可以用来标记当前记录在当前堆的位置,但是从2开始的,因为前面的 0 和 1 分别为infimum和supremum。
刚才说了插入和删除,那么查找呢?这就要用到数据结构中的分块索引了,本来一页的数据就不多,但是为了提高查找效率(个人说法),在页内还采用了分块索引。这个关键码存储的地方就是Page Direction,也挺形象的,就是这一页的目录。分块索引再说一下:
- 块内最大关键码:就是将数据记录分组后,每个组内的最后一条记录,也就是最大的那条(因为已经按主键排序),其地址偏移量;
- 块中记录个数:就每条记录头信息中,有个n_owned属性,就是该记录有多少条记录,就是该块内(组内)有多少记录;
- 用于指向块首元素的指针:就是最大关键码对应的next_record属性嘛;
当然,在mysql中不叫块,叫slot(槽)。这样就可以使用二分查找了。
再复习一下分块索引的特点:
块间有序,块内无序。不过由于记录原先就是按照主键排序的,所以在这里面块内也是有序的。
上面说了一下各行记录之间的连接(单链表)和增删查,还有一个问题是:页是内存和磁盘之间数据交换的基本单位,内存,就涉及到一个掉电就丢失的问题,所以存在一个需要校验的过程。由于页是从头开始读的,在处理完毕,开始同步的时候,File Header中算出来一个校验和,,到File Tail的时候,再算出来一个校验和,看是否一致,如果没有意外应该是一致的,二者不同就意味着同步出了问题。
再放一个图:
好了,从上往下捋一下:
- File Head,38个字节,表示页的一些基本信息,比如上一页,下一页的双指针链表;
- Page Head,56个字节,本数据页中存储记录的状态信息,比如本页中已经存储了多少信息,第一条记录的地址是多少,Page Direction中存了多少slot等;
- Infimum + supremum,26个字节,这是最小行和最大行,我个人感觉就相当于单链表的头和尾;
- User Records:大小不固定,真实存储插入的数据记录;
- Free Space:大小不固定,页中尚未使用的部分;
- Page Direction:大小不固定,分块索引的关键码索引,插入的记录越多,占用空间越多;
- File Tailer:8个字节,检验页是否完整的部分;