|
|
@ -185,8 +185,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 索引优化
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 64. 深入研究索引之前,先来看看磁盘数据页的存储结构
|
|
|
|
|
|
|
|
- 大量的数据页是按顺序一页一页存放的,然后两两相邻的数据页之间会采用双向链表的格式互相引用
|
|
|
|
|
|
|
|
- 上述表示在磁盘中的形态
|
|
|
|
|
|
|
|
- 其实一个数据页在磁盘文件里就是一段数据,可能是二进制或者别的特殊格式的数据,然后数据页里包含两个指针,一个指针指向自己上一个数据页的物理地址,
|
|
|
|
|
|
|
|
一个指针指向自己下一个数据页的物理地址,大概可以认为类似下面这样:
|
|
|
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=15367, linked_list_next_pointer=34126 ||
|
|
|
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=23789, linked_list_next_pointer=46589 ||
|
|
|
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=33198, linked_list_next_pointer=55681
|
|
|
|
|
|
|
|
- MySQL实际存储大致也是类似这样的,就是每个数据页在磁盘文件里都是连续的一段数据。
|
|
|
|
|
|
|
|
然后每个数据页里,可以认为就是DataPage打头一直到 || 符号的一段磁盘里的连续的数据,你可以认为每一个数据页就是磁盘文件里这么一段连续的东西。
|
|
|
|
|
|
|
|
每个数据页,都有一个指针指向自己上一个数据页在磁盘文件里的起始物理位置,比如 linked_list_pre_pointer=15367 就是指向了上一个数据页在磁盘文件里的起始物理位置
|
|
|
|
|
|
|
|
那个15367可以认为就是在磁盘文件里的position或者offset,同理,也有一个指针指向自己下一个数据页的物理位置。
|
|
|
|
|
|
|
|
- 然后一个数据页内部会存储一行一行的数据,也就是平时我们在一个表里插入的一行一行的数据就会存储在数据页里,然后数据页里的每一行数据都会按照主键大小进行排序存储,同时每一行数据都有指针
|
|
|
|
|
|
|
|
指向下一行数据的位置,组成单向链表
|
|
|
|
|
|
|
|
![磁盘数据页的存储结构](pic/磁盘数据页的存储结构.png)
|
|
|
|
|
|
|
|
- 总结: 数据页之间是组成双向链表的,然后数据页内部的数据行是组成单向链表的,而且数据行是根据主键从小到大排序的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 65. 假设没有任何索引,数据库是如何根据查询语句搜索数据的?
|
|
|
|
|
|
|
|
- 每个数据页里都会有一个页目录,里面根据数据行的主键存放了一个目录,同时数据行是被分散存储到不同的槽位里去的,所以实际上每个数据页的目录里,就是这个页里每个主键跟所在槽位的映射关系
|
|
|
|
|
|
|
|
![页目录1](pic/页目录1.png)
|
|
|
|
|
|
|
|
- 假设你要根据主键查找一条数据,而且假设此时你数据库里那个表就没几条数据,那个表总共就一个数据页,首先就会先到数据页的页目录里根据主键进行二分查找
|
|
|
|
|
|
|
|
- 然后通过二分查找在目录里迅速定位到主键对应的数据是在哪个槽位里,然后到那个槽位里去,遍历槽位里每一行数据,就能快速找到那个主键对应的数据了。
|
|
|
|
|
|
|
|
- 每个槽位里都有一组数据行,你就是在里面遍 历查找就可以了。
|
|
|
|
|
|
|
|
- 但是假设你要是根据非主键的其他字段查找数据呢?
|
|
|
|
|
|
|
|
- 此时你是没办法使用主键的那种页目录来二分查找的,只能进入到数据页里,根据单向链表依次遍历查找数据了,这就性能很差了。
|
|
|
|
|
|
|
|
- 假如我们有很多数据页呢?
|
|
|
|
|
|
|
|
- 假设你要是没有建立任何索引,那么无论是根据主键查询,还是根据其他字段来条件查询,实际上都没有什么取巧的办法
|
|
|
|
|
|
|
|
- 一个表里所有数据页都是组成双向链表的吧?好,有链表就好办了,直接从第一个数据页开始遍历所有数据页,从第一个数据页开始,
|
|
|
|
|
|
|
|
你得先把第一个数据页从磁盘上读取到内存buffer pool的缓存页里来。
|
|
|
|
|
|
|
|
- 然后你就在第一个数据页对应的缓存页里,按照上述办法查找
|
|
|
|
|
|
|
|
- 假设是根据主键查找的,你可以在数据页的页目录里二分查找,假设你要是根据其他字段查找的,只能是根据数据页内部的单向链表来遍历查找
|
|
|
|
|
|
|
|
![页目录2](pic/页目录2.png)
|
|
|
|
|
|
|
|
- 假设第一个数据页没找到你要的那条数据
|
|
|
|
|
|
|
|
- 只能根据数据页的双向链表去找下一个数据页,然后读取到buffer pool的缓存页里去,然后按一样的方法在一个缓存页内部查找那条数据。
|
|
|
|
|
|
|
|
- 如果依然还是查找不到呢?
|
|
|
|
|
|
|
|
- 那只能根据双向链表继续加载下一个数据页到缓存页里来了,以此类推,循环往复。
|
|
|
|
|
|
|
|
- 你似乎是在做一个数据库里很尴尬的操作:全表扫描?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 66. 不断在表中插入数据时,物理存储是如何进行页分裂的?
|
|
|
|
|
|
|
|
- 我们在一个表里不停的插入数据的时候,会涉及到一个页分裂的过程,也就是说,这个表里是如何出现一个又一个的数据页的。
|
|
|
|
|
|
|
|
- 正常情况下我们在一个表里插入一些数据后,他们都会进入到一个数据页里去,在数据页内部,他们会组成一个单向链表,这个数据页内部的单向链表大致如下所示
|
|
|
|
|
|
|
|
![页分裂1](pic/页分裂1.png)
|
|
|
|
|
|
|
|
- 里面就是一行一行的数据,刚开始第一行是个起始行,他的行类型是2,就是最小的一行
|
|
|
|
|
|
|
|
- 他有一个指针指向了下一行数据,每一行数据都有自己每个字段的值,然后每一行通过一个指针不停的指向下一行数据
|
|
|
|
|
|
|
|
- 普通的数据行的类型都是0,最后一行是一个类型为3的,就是代表最大的一行。
|
|
|
|
|
|
|
|
- 假设你不停的在表里插入数据,那么刚开始是不是就是不停的在一个数据页插入数据?接着数据越来越多,越来越多,此时就要再搞一个数据页了
|
|
|
|
|
|
|
|
![页分裂2](pic/页分裂2.png)
|
|
|
|
|
|
|
|
- 索引运作的一个核心基础就是要求你后一个数据页的主键值都大于前面一个数据页的主键值
|
|
|
|
|
|
|
|
- 但是如果你的主键是自增的,那还可以保证这一点,因为你新插入后一个数据页的主键值一定都大于前一个数据页的主键值
|
|
|
|
|
|
|
|
- 但是有时候你的主键并不是自增长的,所以可能会出现你后一个数据页的主键值里,有的主键是小于前一个数据页的主键值的
|
|
|
|
|
|
|
|
- 比如在第一个数据页里有一条数据的主键是10,第二个数据页里居然有一条数据的主键值是8,那此时肯定有问题了。
|
|
|
|
|
|
|
|
- 所以此时就会出现一个过程,叫做页分裂
|
|
|
|
|
|
|
|
- 就是万一你的主键值都是你自己设置的,那么在增加一个新的数据页的时候,实际上会把前一个数据页里主键值较大的,挪动到新的数据页里来,然后把你新插入
|
|
|
|
|
|
|
|
的主键值较小的数据挪动到上一个数据页里去,保证新数据页里的主键值一定都比上一个数据页里的主键值大。
|
|
|
|
|
|
|
|
- 假设新数据页里,有两条数据的主键值明显是小于上一个数据页的主键值的
|
|
|
|
|
|
|
|
![页分裂3](pic/页分裂3.png)
|
|
|
|
|
|
|
|
- 第一个数据页里有1、5、6三条数据,第二个数据页里有2、3、4三条数据,明显第二个数据页里的数据的主键值比第一个数据页里的5和6两个主键都小,所以这个是不行的
|
|
|
|
|
|
|
|
- 此时就会出现页分裂的行为,把新数据页里的两条数据挪动到上一个数据页,上一个数据页里挪两条数据到新数据页里去
|
|
|
|
|
|
|
|
![页分裂4](pic/页分裂4.png)
|
|
|
|
|
|
|
|
- 这就是一个页分裂的过程,核心目标就是保证下一个数据页里的主键值都比上一个数据页里的主键值要大。
|
|
|
|
|
|
|
|
- 保证了每个数据页的主键值,就能为后续的 索引打下基础
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|