MYSQL实战优化——索引介绍

初步了解索引

之前我们介绍过,数据页在磁盘文件中的物理存储结构,数据页之间是组成双向链表的,然后数据页内部的数据行是组成单向链表的,而且数据行是根据主键从小到大排序的。然后每个数据页里都会有一个页目录,里面根据数据行的主键存放了一个目录,同时数据行是被分散存储到不同的槽位里去的,所以实际上每个数据页的目录里,就是这个页里每个主键跟所在槽位的映射关系,如下图所示:


index1.jpg

假设你要根据主键查找一条数据,而且假设数据库里那个表没几条数据,那个表总共就一个数据页,那么就太简单了,首先就会先找到数据页的页目录里根据主键进行二分查找。然后通过二分查找在目录里迅速定位到主键对应的数据是在哪个槽位里,然后到那个槽位里去遍历每一行数据,就能快速找到那个主键对应的数据了。

但是假设你要根据非主键的其它字段查找数据呢?
那就尴尬了,此时你是没办法使用主键的那种页目录来二分查找的,只能进入到数据页里,根据单向链表依次遍历查找数据了,这就性能很差了。那么现在假如我们有很多数据呢?比如成百上千个数据页?这就没有什么好的办法,只能根据数据页链表一个数据页一个数据页的去查找,就是我们常说的全表扫描。

页分裂

大家都知道,正常情况下我们在一个表里插入一些数据后,他们都会进入到一个数据页里,在数据页内部,它们会组成一个单项链表,这个数据页内部的单向链表大致如下所示:


index2.jpg

在上述图中,里面就是一行一行的数据,刚开始第一行是个起始行,它的类型是2,就是最小的一行,然后它有一个指针指向了下一行数据,每一行数据都有自己每个字段的值,然后每一行通过一个指针指向下一行数据,普通的数据行的类型都是0,最后一行类型为3,就是代表最大一行。

现在假设你不停的在表里插入数据,那么刚开始不停的在一个数据页插入数据,接着数据越来越多,此时就要再搞一个数据页了。但是此时会遇到一个问题,之后会介绍索引机制,索引运作的一个核心基础就是要求你后一个数据页的主键值都大于前面一个数据页的主键值,但是如果你的主键是自增的,那还可以保证这一点,因为你新插入后一个数据页的主键值一定都大于前面一个数据页的主键值。但是,有时候你的主键并不是自动增长的,所以可能会出现后一个数据页的主键值里,有的主键是小于前一个数据页的主键值的。

所以此时就会出现一个过程,叫做页分裂,就是万一你的主键值都是你自己设置的,那么在增加一个新的数据页的时候,实际上会把前一个数据页里主键值较大的,挪动到新的数据页里来,然后把你新插入的主键值较小的数据挪动到上一个数据页里去,保证新数据页里的主键值一定都比上一个数据页里的主键值大。

主键索引

假设我们有很多个数据页,然后我们想要根据主键来查询数据,那么直接查询的话也是不行的,因为我们也不知道主键到底是在哪里。比如说你要查找id=4的数据,你怎么知道在哪个数据页里?没有任何证据可以告诉你它到底是在哪个数据页里。所以如果是这个样子的话,也就只能全表扫描了,这肯定是不能接受的。

所以此时就需要针对主键设计一个索引了,针对主键的索引实际上就是主键目录,就是把每个数据页的页号,还有数据页里最小的主键值放在一起,组成一个索引的目录。如下图所示:

index3.jpg

现在我们有了上图的主键目录就方便了,直接就可以到主键目录里去搜索,比如你要找id=3的数据,此时就会跟每个数据页的最小主键来比,首先id=3大于了数据页1里的最小主键值1,接着小于了数据页2里的最小主键值4。所以既然如此,你直接就可以定位到id=3的数据一定是在数据页1里面。

如果你有很多的数据页,在主键目录里就会有很多的数据页和最小主键值,此时你完全可以根据二分查找的方式来找你要找的id到底在哪个数据页里。所以这个效率是非常之高的,而类似上图的主键目录,就可以认为是主键索引。

B+实现索引的页存储物理结构

上面我们说了主键索引的目录结构和查找方法,但是其实是有问题的,如果你的表里的数据很多,比如有几千万,甚至单表几亿条数据都是有可能的,所以此时有大量的数据页,然后主键目录里存储大量的数据页页号和最小主键值,这怎么办呢?所以在考虑这个问题的时候,实际上是采取了一种把索引数据存储在数据页里的方式来做的。

也就是说,你的表的实际数据是存放在数据页里的,然后你的表的索引也是存放在页里的,此时索引放在页里之后,就会有索引页,假设你有很多的数据页,那么此时你就可以有很多的索引页。但是现在又会存在一个问题了,你现在有很多索引页,但是此时你需要知道你应该到哪个索引页里去找你的主键数据,是索引页20?还是索引页28?这也是个大问题。于是接下来我们又可以把索引页多加一个层级出来,在更高的索引层级里,保存了每个索引页和索引页里的最小主键值,如下图所示:


index4.jpg

假设我们要查询id=46的数据,直接先到最顶层的索引页35里去找,直接通过二分查找可以定位到下一步应该到索引页20里去找,接下来到索引页20里通过二分查找定位,也很快可以定位到数据应该在数据页2里,这样就可以找到id=46的那行数据了。那么现在问题再次来了,假如你最顶层的那个索引页里存放的下层索引页的页号也太多了,怎么办呢?此时可以再次分裂,再加一层索引页,比如下图那样:

index5.jpg

现在搞的是不是有点像一棵树了?没错,这就是一颗B+树,属于数据结构里的一种树形数据结构,所以一直说MySQL的索引是用B+树来组成的。

B+实现索引的页存储物理结构

我们上面讲了如何基于索引数据结构去查找主键的过程,那么大家有没有发现一件事情,就是最下层的索引页,都是会有指针引用数据页的,所以实际上索引页跟数据页之间是有指针连接起来的。另外,索引页自己内部,对于一个层级内的索引页,互相之间都是基于指针组成双向链表的。如下图:


index6.jpg

在上图这个B+树里,最底层的一层就是数据页,数据页也就是B+树里的叶子节点。所以:

如果一颗大的B+树索引数据结构里,叶子节点就是数据页自己本身,那么此时我们就可以称这颗B+树索引为聚簇索引。

其实在innodb存储引擎里,你在对数据增删改的时候,就是直接把你的数据页放在聚簇索引里的,数据就在聚簇索引里。如果你的数据页开始进行页分裂了,它此时会调整各个数据页内部的行数据,保证数据页内的主键值都是有顺序的,下一个数据页的所有主键值大于上一个数据页的所有主键值。

同时在分裂的时候,会维护你的上层索引数据结构,在上层索引页里维护你的索引条目,不同的数据页号和最小主键值。另外,这个聚簇索引默认是按照主键来组织的,所有在增删改数据的时候,一方面会更新数据页,一方面会给你字段维护B+树结构的聚簇索引。其实聚簇索引就是innodb存储引擎默认给我们创建的一套基于主键的索引结构,而且我们表里的数据就是直接放在聚簇索引里的,作为叶子节点的数据页。

主键之外的字段索引原理

假设你要针对其它字段建立索引,比如name、age之类的字段,这都是一样的原理,比如你插入数据的时候,一方面会把完整数据插入到聚簇索引的叶子节点的数据页里去,同时维护好聚簇索引,另一方面为其它字段建立索引,就是重新再建立一颗B+树。

比如基于name字段建立了一个索引,那么此时你插入数据的时候就会重新搞一颗B+树,B+树的叶子节点也是数据页,但是这个数据页里仅仅放主键字段和name字段。至于排序规则之类的,跟之前说的一样。也就是说,name字段的索引B+树里,叶子节点的数据页中的name值都是按大小排序的,就是下一个数据页里的name字段值都是大于上一个数据页里的name字段值,这个整体的排序规则都跟聚簇索引安照主键的排序规则是一样的。另外,name字段的B+树建造也跟聚簇索引是一样的,这里就不再重复介绍。

假设你要根据name字段来搜索数据,那搜索过程都一样,就是从name字段的索引B+树里的根节点开始找,一层一层往下找,一直找到叶子节点的数据页里,定位到name字段值对应的主键值。然后,此时针对select * from table where name='xxx'这样的语句,先根据name字段值在name字段的索引B+树里找,找到叶子节点也仅仅可以找到对应的主键值,而找不到这行数据完整的所有字段。

所以此时还需要进行,就是说还需要根据主键再到聚簇索引里从根节点开始,一路找到叶子节点的数据页,定位到主键对应的完整数据行,此时才能把select *要的全部字段值都拿出来。所以一般把name字段这种普通字段的索引称之为,一级索引就是聚簇索引,这就是普通字段的索引运行原理。

其实我们也可以把多个字段联合起来,建立联合索引,比如name+age。这时候叶子节点的数据页里放的是id+name+age,然后默认按照name排序,name一样就按照age排序,不同数据页之间的name+age值的排序也如此。其它的就都跟二级索引是一样的,就不再重复。

不同索引B+树的维护

首先,刚开始一个表建好之后,其实它就一个数据页,这个数据页就是属于聚簇索引的一部分,而且目前还是空的。此时如果你插入数据,就是直接在这个数据页里插入就可以了,也没必要给它弄什么索引页。这个初始的数据页其实就是一个每个数据页内部默认就有一个基于主键的页目录,所以根据主键来搜索都是ok的,直接在唯一一个数据页里根据页目录找就行了。

之后表里的数据会越来越多,此时你的数据页满了,那么就会高一个新的数据页,然后把你根页面里的数据都拷贝过去,同时再搞一个新的数据页,根据你的主键值的大小进行挪动,让两个新的数据页根据主键值排序,第二个数据页的主键值都大于第一个数据页的主键值。

那么此时那个根页在哪儿呢?此时根页就升级为了索引页,这个根页里放的是数据页的页号和它们里面最小的主键值。当数据越来越多,索引条目也会越来越多,一个索引页已经放不下了,那就分裂成两个索引页,然后根页继续往上走一个层级。以此类推,就会有越来越多的索引页生成,根页就继续往上走。这其实就是增删改的时候,整个聚簇索引维护的一个过程,其它的二级索引也是类似的一个原理。

你可能感兴趣的:(MYSQL实战优化——索引介绍)