InnoDB是如何存储数据的
千里之行,始于足下。
—— 老子
MySQL支持多种存储引擎,不同的存储引擎,存储数据的方式也是不同的,我们最常使用的是InnoDB存储引擎。
记录是按照 行 来存储的,但是数据库的读取并不以 行 为单位,否则一次读取(也就是一次I/O操作)只能处理一行数据,效率会非常低。
因此,InnoDB的数据是按 数据页 为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以 页 为单位,将其整体读入内存。
数据库的I/O 操作的最小单位是 页 ,InnoDB数据页的默认大小是16KB,意味着数据库每次读写都是以16KB为单位的,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16K内容刷新到磁盘中。
数据页包括七个部分:
这7个部分的作用如下图:
在File Header中有两个指针,分别指向上一个数据页和下一个数据页,连接起来的页相当于一个双向的链表,如下图:
采用链表的结构是让数据页之间不需要是物理上的连续的,而是逻辑上的连续。
数据页的主要作用是存储记录,也就是数据库的数据,所以要搞清楚数据页中的User Records是怎么组织数据的。
数据页中的记录按照 主键 顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。
因此,数据页中有一个 页目录,起到记录的索引作用,就像书那样,针对书的内容的每个章节设立了一个目录,想看某些章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。
InnoDB是如何给记录创建页目录呢?页目录与记录的关系如下图:
页目录创建的过程如下:
1.将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
2.每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息会存储该组一共有多少条记录,作为n_owned字段(上图中粉红色字段)
3.页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照 主键值 从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位到要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。
以上面那张图举个例子,5个槽的编号分别为0,1,2,3,4,我想查找主键为11的用户记录:
- 先二分的出槽中间位是(0+4)/2 =2,2号槽里最大的记录为8。因为11 > 8,所以需要从2号槽后继续搜索记录;
- 再使用二分搜索出2号和4号槽的中间位是(2+4)/2=3,3号槽里最大记录为12,因为11<12,所以主键为11的记录在3号槽里;
- 这里有个问题,槽对应的值都是这个组的主键最大的记录,如何找到组里最小的记录?比如槽3对应最大主键是12的记录,那如何找到最小记录9。解决办法是:通过槽3找到槽2对应的记录,也就是主键为8的记录,主键为8的记录的下一条记录就是槽3当中主键最小的9记录,然后开始向下搜索2次,定位到主键为11的记录,取出该条记录的信息即为我们想要查找的内容。
如果某个槽内的记录很多,然后因为记录的都是单向链表串起来的,那这样在槽内查找某个记录的时间复杂度不就是O(n)了吗?
InnoDB对每个分组中的记录条数都是有规定的,槽内的记录就只有几条:
- 第一个分组中的记录只能有1条记录;
- 最后一个分组中的记录条数范围只能在1-8条之间;
- 剩下的分组中记录条数范围只能在4-8条之间;