侧边栏壁纸
博主头像
ProSayJ 博主等级

Talk is cheap. Show me the code.

  • 累计撰写 43 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

33-深入研究索引-01

YangJian
2025-06-28 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

1. 磁盘数据页的存储结构

在此之前, 我们已经初步介绍了MySQL数据库的一些内核原理,包括更新语句的执行原理、事务原理以及锁机制的实现。

接下来,我们将进入一个非常关键的环节,那就是数据库索引原理和查询优化。掌握了这部分内容后,我们将能够深入学习大量的实战案例,包括索引设计查询调优等内容。

然而,在深入讨论索引之前,我们首先需要了解磁盘上数据文件中的数据页的物理存储结构,因为后续我们研究索引的物理存储方式和使用原理时,都会与数据页的存储结构紧密相关。

数据库中的所有数据(包括我们创建的各种表和表里的数据)最终都是存储在磁盘上的文件中的,而这些文件中的存储格式就是数据页。那么,这些数据页在磁盘文件里是如何组织存储的呢?

首先,重要的一点是,大量的数据页是按照顺序依次存储的,每一页的数据都是连续存放的,而相邻的两页数据之间会通过双向链表的方式进行引用。大致可以这样理解:每个数据页不仅包含了存储的数据,还会保存指向前后页的指针,以便于磁盘上的数据能够快速查找和访问。

不过,这些数据页在磁盘文件里是如何存储和组织的呢?

实际上,一个数据页在磁盘文件里就是一段数据,可能是二进制数据或者其他特定格式的数据。在这个数据页中,通常会包含两个指针,一个指针指向自己前一个数据页的物理地址,另一个指针则指向自己后一个数据页的物理地址。你可以大致理解为,数据页通过这两个指针实现了“前后连接”的效果,类似于下面这样:

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数据库磁盘文件中的真实存储格式,但我想通过类似的例子让你更好理解。实际上,MySQL的存储方式也与此相似——每个数据页在磁盘文件里都是一段连续的存储区域。

每个数据页的存储内容可以视为从 DataPage 开始,到 || 符号结束的一段连续数据。你可以将每个数据页理解为磁盘文件中的一个连续区块。

此外,每个数据页内部都会有一个指针,指向自己上一个数据页在磁盘中的物理位置。例如,linked_list_pre_pointer=15367 表示指向上一个数据页的起始物理位置,其中15367可以看作是磁盘文件中的偏移量(position或offset)。同样,数据页也会有一个指针指向下一个数据页的位置。

现在回头看看上面的图,你就能理解磁盘文件里的多个数据页是如何通过指针构成一个双向链表的。

接下来,每个数据页内部会存储一行一行的数据,也就是我们平时在表中插入的记录。这些数据会按照主键的大小进行排序存储。每一行数据内部也会有指针,指向下一行数据的位置,形成一个单向链表,类似如下图所示。

这样的存储结构使得数据能够有效地被管理和访问,同时也为我们后续讲解索引和查询优化提供了基础。


2. 假设没有任何索引,数据库是如何根据查询语句搜索数据的

现在,大家应该清楚地知道,数据页之间是通过双向链表连接的,而每个数据页内部的数据行则是组成单向链表的,并且这些数据行是按照主键从小到大的顺序进行排序的。

另外,每个数据页里还会有一个页目录,它记录了每一行数据的主键,并与该数据行在数据页中的槽位进行映射。实际上,数据页的目录就是为数据页中的每个主键和它所在的槽位建立了一个对应关系。如下图所示,目录中的每一项都会关联到具体的槽位,进而指向数据行的位置。

这样我们就能够清晰地理解数据页的组织结构,并为接下来的索引设计和查询优化打下基础。

假设你要根据主键来查找一条数据,如果此时数据库表中的数据量不大,只有一个数据页,那么查询就非常简单!首先,数据库会通过数据页中的页目录,利用二分查找来快速定位到目标主键对应的数据。注:如果你对二分查找不熟悉,建议查阅相关资料,它是大学里非常基础的算法之一。

通过二分查找,数据库能够迅速在目录中找到主键对应的数据所在槽位。接着,数据库会进入该槽位,遍历其中的每一行数据,最终找到匹配的主键。

然而,假设你需要根据非主键字段来查找数据,情况就不太一样了。这时,你无法利用主键的页目录来进行二分查找,而是需要进入数据页,通过单向链表逐一遍历数据行。这种方式的性能相对较差。

那么,如果表中的数据量增多了,数据页也变得更多呢?

通常来说,一个表会有大量的数据页,可能多达数百甚至数千个数据页。这些数据页会存储在物理磁盘文件中。那么,在这种情况下,数据库是如何进行查询的呢?

如果你没有建立任何索引,查询数据就非常低效。无论是按主键查询,还是按其他字段查询,都没有什么优化手段。所有的数据页在磁盘上是以双向链表的形式存储的,而这些数据页存储在磁盘文件中。

假设你要进行查询,首先得从第一个数据页开始遍历。如果表的数据很多,数据页也会非常多。此时,你需要先将第一个数据页从磁盘加载到内存的缓存区域(Buffer Pool)中,然后才能开始查找。

具体来说,如果你是按主键查询,数据页会有一个页目录,你可以在目录中使用二分查找来快速定位。如果查询条件是非主键字段,那就不那么幸运了,因为此时你只能通过遍历数据页内部的单向链表来找到对应的数据。

这种做法效率很低,尤其当数据量非常大的时候,查询性能就会变得相当糟糕。

假设在前面的情况中,第一个数据页没有找到你需要的数据,那么就只能根据数据页的双向链表找到下一个数据页,并将其加载到缓存区(Buffer Pool)中,然后再按照相同的方式在缓存页里查找。

如果依然没有找到,那么你只能继续通过双向链表去加载下一个数据页,依此类推,直到遍历所有数据页。

看完这个过程后,你可能会有些感想——这是不是在做一个非常低效的操作?没错!这就是数据库的“全表扫描”。

在没有任何索引数据结构的情况下,无论你如何查找,实际上你都在做全表扫描。就是通过双向链表把磁盘中的每个数据页依次加载到缓存页中,然后在每个缓存页内查找目标数据。

最坏的情况下,你可能要遍历所有数据页中的每条数据,才能找到你需要的那一条数据。这样就非常低效。

接下来,随着我们进入索引的 讨论,你将真正理解索引是如何提升数据库查询效率的!

3. 不断在表中插入数据时,物理存储是如何进行页分裂的?

我们上面讨论了数据页的物理存储结构,数据页之间形成了双向链表,数据页内部的每条数据行又形成了单向链表,而且每个数据页内还会有一个根据主键排序的页目录。

在没有索引的情况下,所有的数据查询基本上都是在物理层面上进行全表扫描。也就是说,数据库会逐个扫描每个数据页,并在每个数据页内部逐行查找数据。

这个过程可以说是非常慢的,因此在实际应用中,我们一定不能让查询走全表扫描。为了加速查询的执行,通常必须使用索引。

但是现在我们还不能直接进入索引的讨论,因为在深入讨论索引之前,我们需要先了解一个非常重要的概念——页分裂。也就是说,我们要了解在一个表中不断插入数据时,数据是如何在不同的数据页之间分配的。

正常情况下,插入的数据会进入一个数据页,在数据页内部,数据行会按照一定的顺序组成一个单向链表。

接下来,我们来看看这个过程是如何发生的。

大家看到上面的图,图中的每一行代表数据,最开始的第一行是起始行,它的行类型是2,表示它是最小的一行。每行数据都有自己的字段值,并且通过指针指向下一行数据。普通数据行的类型通常是0,而最后一行则是类型为3,表示它是最大的一行

这就是一个典型的数据页内部的情况。那么,我们要讲的“页分裂”到底是什么意思呢?

其实,页分裂是指当我们在表里不断插入数据时,最开始数据会被插入到一个数据页里。当数据量逐渐增多,填满这个数据页后,就必须再创建一个新的数据页,如下图所示。

但此时会遇到一个问题。后续我们会讲到索引机制,索引运作的核心基础之一就是要求后一个数据页的主键值大于前一个数据页的主键值。如果你的主键是自增的,那么这个规则是自然能保证的,因为新插入的数据页主键值总是比前一个数据页的主键值要大。

然而,如果你的主键不是自增的,那么可能会出现一个情况:后一个数据页的主键值有时小于前一个数据页的主键值。

比如,在第一个数据页里有一条数据的主键是10,而第二个数据页里却有一条主键为8的数据,那么这种情况显然是不符合要求的。

这时,就会出现一个过程叫做页分裂。如果你的主键值是你手动设置的,那么在增加新的数据页时,系统会将前一个数据页中主键值较大的数据移动到新的数据页中,同时把新插入的主键值较小的数据保留在前一个数据页里,这样可以确保新数据页中的主键值始终大于前一个数据页中的主键值。

大家可以看下图,假设新数据页中有两条数据的主键值明显小于上一个数据页的主键值,如图所示。

如上图所示,假设第一个数据页里包含数据1、5、6,而第二个数据页里包含数据2、3、4,显然,第二个数据页中的数据的主键值比第一个数据页中的5和6要小,这是不符合索引存储规则的。

此时,就会出现页分裂的行为:为了确保数据页内的主键值顺序正确,系统会将第二个数据页中的两条数据(2和3)移动到第一个数据页中,然后将第一个数据页中主键值较大的两条数据(5和6)移到新的数据页里。这样,两个数据页中的主键值就会严格按照从小到大的顺序排列。

这个过程确保了数据页间的主键值顺序正确,同时也避免了因主键冲突而带来的问题。

页分裂的过程正是确保数据库表中每个数据页的主键值严格按顺序排列,从而为后续索引的构建奠定了基础。页分裂不仅仅是为了保证数据的一致性和顺序性,还为数据库的性能优化,特别是在查询时的效率提升,提供了关键保障。

通过页分裂,数据库能够避免出现数据页之间主键值混乱的情况,从而使得查询和索引构建能够更加高效。

4. 基于主键的索引是如何设计的,以及如何根据主键索引查询?

上面我们讨论了数据页分裂的过程,说明在你不断向表中插入数据时,数据库会生成一个个数据页。如果主键不是自增的,可能会有数据行的移动,确保新数据页中的主键值大于前一个数据页的主键值。

在理解了数据页的分裂过程后,我们现在终于可以正式开始分析索引的原理了。我们将从最基础的主键索引开始,逐步讨论索引的工作原理以及查询过程,之后我们会通过实际的索引设计案例和SQL优化案例进一步深入讨论。

现在,假设我们有多个数据页,并且需要根据主键来查询数据。问题是,直接查询也不是那么简单,因为我们并不知道主键到底存储在哪个数据页上,对吧?

回顾一下上面的图:

现在,假设要查找 id=4 的数据,你该怎么知道它存储在哪个数据页里呢?如果没有任何线索或结构指示,实际上你无法得知它所在的具体数据页。

如果情况依旧如此,那么只能进行全表扫描,从第一个数据页开始,逐一进入每个数据页的页目录查找主键。最坏的情况是,得遍历所有数据页才能找到目标数据,效率可想而知,还是非常低效。

这时就需要为主键设计一个索引了。主键的索引实际上就是一个“主键目录”,它会将每个数据页的页号和该数据页中最小的主键值一起存储,组成一个索引目录。这样做的目的是通过索引目录来快速定位数据页,提高查询效率。如下图所示。

现在,有了上图的主键目录,我们的查询就方便多了。假设你要找 id=3 的数据,可以直接在主键目录中进行查找。首先,通过与每个数据页的最小主键值进行比较,发现 id=3 大于数据页 2 中的最小主键值 1,而小于数据页 8 中的最小主键值 4。

因此,可以直接确定 id=3 的数据一定位于数据页 2 中!

如果数据页很多,主键目录中将包含大量数据页的最小主键值,此时可以通过二分查找法快速定位数据所在的具体数据页。这个查找过程的效率非常高,相比全表扫描快得多。

这种结构实际上就是一个“主键索引”。主键索引作为一个目录,帮助我们迅速定位到数据所在的数据页,大大提高了查询效率。

我们的数据库数据页通常是按块连续存放在磁盘文件中的。因此,在通过主键索引定位到数据页后,假设我们还有一种方式记录数据页和磁盘文件的对应关系,你就可以很轻松地通过定位到磁盘文件的某个位置(即 offset 偏移量)来实现快速查询。通过随机读的方式,你就能直接跳转到磁盘文件的特定位置,读取到对应的数据页。

以上就是索引的最最基础概念。也许你觉得索引看起来并不复杂,但这只是索引的入门篇。😏😏😏好戏马上开始~~~~

5. ♨🙋‍♂️🙋‍♂️♨索引的页存储物理结构,是如何用B+树来实现的?

B+ Tree Visualization

上面我们讨论了主键索引的目录结构。只要在主键索引中包含每个数据页及其对应的最小主键值,就能构成一个索引目录。这样,查询主键时,你可以在这个目录中进行二分查找,快速定位到数据所属的数据页。之后,再在对应的数据页内通过二分查找来准确定位到目标数据。

但是,问题来了。假设你的表中数据量非常庞大,比如几百万、几千万,甚至几亿条数据。那么,这时你可能会有大量的数据页,而主键目录中必须存储这些数据页和最小主键值,这样的结构会非常庞大,如何处理呢?

为了解决这个问题,实际上采取了一种方式,将索引数据存储在数据页中也就是说,表中的实际数据存储在数据页里,表的索引同样也是存储在数据页中。索引页、数据页二合一!! 这样,索引就会存放在索引页(数据页)中。如果你有大量的数据页,那么你就会同时有了多个索引页。如下图所示。

但现在又出现了一个新问题:你有很多索引页,这意味着你需要知道应该去哪个索引页查找你的主键数据。是索引页20?还是索引页28?这也是个大问题。

为了应对这个问题,我们可以再增加一个索引层级。在这个更高的索引层级中,保存了每个索引页以及索引页中最小主键值的映射。这样,我们就可以通过这个更高层次的索引来快速定位到具体的索引页。如下图所示。

现在情况已经好多了,假设我们要查找id=46的,首先从最顶层的索引页35开始,使用二分查找可以快速定位到应该去索引页20查找。接着,在索引页20里通过二分查找就能很快定位到数据应该存储在数据页8中,最后进入数据页8,就能找到id=46对应的那行数据。

然而问题再次出现了:如果最顶层的索引页里存储的下层索引页的页号也太多了,怎么办呢?

此时,我们可以再次进行分裂,增加一层新的索引页,如下图所示。

有没有注意到? 索引页逐渐组成了多个层级,看起来像一棵树?没错,这就是B+树——一种树形数据结构。因此,当我们说MySQL的索引是基于B+树构建的,实际上指的就是这个原理。

以最简单的主键索引为例,建立主键索引后,实际上就是在构建一颗B+树。查询数据时,你从B+树的顶层开始,通过二分查找逐层向下定位,最终进入数据页,再在数据页内部通过二分查找找到目标数据。

这种结构就是索引的真实物理存储方式,采用与数据页类似的存储方式,把索引分散到多个页,构成一棵B+树。

目前为止: 我们对索引的基本概念有了个初步了解,接下来我们将深入探讨各种索引的物理存储原理。

理解了索引之后,后续的查询原理和执行计划将变得更容易理解。因为查询过程,本质上就是通过不同类型的索引来高效查找数据。

6. 更新数据的时候,自动维护的聚簇索引到底是什么?

上面讨论的都是 如何基于主键构建索引,建立索引后,通过索引可以快速定位到数据所在的数据页,然后进入数据页后,通过目录快速找到目标数据。可以参考下面这张图,进一步理解这一过程。

现在,基于上面的这个之前的图,再次梳理一下通过主键搜索数据的过程,这样大家也能更好地理解本节的主题: 聚簇索引

首先,假设我们要搜索一个特定主键 id 对应的行,步骤如下:

  1. 我们首先从顶层的索引页(比如页号88)开始。

  2. 通过二分查找的方式,我们可以快速定位到下一步应该访问的下层索引页。

接下来,跟随图示,我们逐步深入查找,最终确定目标数据所在的位置。

比如现在我们已经定位到了下层的索引页35,接下来的步骤是:

  1. 在索引页35里,我们会看到一些索引条目,这些条目对应着下层的索引页(比如页号20、28、59),以及每个索引页里最小的主键值。

  2. 我们接着通过在索引页35里的二分查找,可以快速定位到,下一步应该去哪个下层索引页(20、28、59)继续查找。

通过这种方式,我们逐层定位,最终能够精确地找到目标数据所在的数据页。

在这种情况下,我们通过以下步骤继续查找:

  1. 在索引页59中,存放了下层的索引条目,每个条目记录了数据页的页号(例如数据页2和数据页8)以及数据页中最小的主键值。

  2. 接下来,在索引页59中继续进行二分查找,通过查找数据页2和数据页8的最小主键值,就能快速确定下一步应该定位到哪个数据页,进一步缩小查找范围。

通过这种层级化的查找,我们能逐步缩小范围,最终定位到含有目标数据的实际数据页。


接着,比如进入数据页2后,其中包含了一个页目录,目录记录了每一行数据的主键值和相应的物理位置。

通过在这个目录中进行二分查找,我们可以迅速确定目标主键值所在的物理位置,从而直接在数据页2中找到相应的数据。

这实际上就是利用索引数据结构查找主键的过程。值得注意的是,最底层的索引页通常会有指针指向数据页,因此索引页和数据页之间是通过指针相互连接的,形成了一种层次化的结构。

另外呢,其实索引页自己内部,对于一个层级内的索引页,互相之间都是基于指针组成双向链表的,如下面图示

这种双向链表的结构和数据页之间的双向链表是类似的。在同一层级的索引页之间,通过指针相互连接,使得索引页能够更加高效地进行查找和遍历。


如果你把上面的索引页和数据页结合起来看,可能会发现它们实际上是连接在一起的,形成了一个完整的大B+树。从根索引页88开始,一直到数据页,整个结构就像一颗庞大的B+树。

在这颗B+树中,最底层是数据页,数据页相当于B+树的叶子节点。

所以,如果这颗B+树的叶子节点是数据页本身,我们就可以称这颗B+树为“聚簇索引”。

实际上,在InnoDB存储引擎中,当你进行数据增删改操作时,数据页是直接放在聚簇索引中的,也就是说数据和聚簇索引是紧密相连的。举个例子,当你插入数据时,数据会直接插入到数据页里。

随着数据量的增加,数据页可能会发生分裂。此时,数据页内部的数据会进行调整,确保每个数据页内的主键值是有序的,并且保证一个数据页的主键值始终大于前一个数据页的主键值。

在数据页分裂的同时,索引结构会进行维护,确保上层的索引页更新,记录每个数据页及其最小主键值。

当数据页增多,一个索引页可能放不下所有索引条目时,系统会创建新的索引页,并引入新的上层索引页,新的索引页会记录下层索引页的页号及其最小主键值。

随着数据量的不断增长,索引结构的层级可能会不断增加,但通常一个索引页能存放很多索引条目,因此即使在大数据表中,索引层级一般也不会超过三到四层。

聚簇索引是基于主键组织的,因此在进行增删改数据时,除了更新数据页,系统还会自动维护B+树结构,更新相关的索引页。聚簇索引是MySQL默认建立的索引类型,并会自动维护。

7. 针对主键之外的字段建立的二级索引,又是如何运作的?

在上面的讨论中,我们已经详细地讨论了聚簇索引的概念。实际上,聚簇索引是InnoDB存储引擎默认为我们创建的一种基于主键的索引结构。在这种索引结构中,表里的数据直接存储在聚簇索引中,数据页就是该索引的叶子节点。

换句话说,聚簇索引不仅仅是一个索引,它实际上包含了数据本身。数据在表中通过聚簇索引进行组织和存储,从而优化了数据的存取速度。通过这种结构,我们可以更加高效地访问和操作数据。

我们已经很清楚地了解了如何通过聚簇索引来根据主键进行数据搜索。实际上,查找过程就是从聚簇索引的根节点开始,通过二分查找一路定位到数据页,然后利用数据页目录快速找到对应的主键数据。

但当我们谈到对主键外的其他字段建立索引,特别是多个字段的联合索引时,索引的结构又是怎样的呢?今天我们就来探讨这个问题。

假设我们要为表中的其他字段(比如name、age等)建立索引,原理与主键索引类似。具体来说,在插入数据时,除了将完整的数据插入到聚簇索引的叶子节点(数据页)外,系统还会为这些字段建立单独的索引。

例如,当我们为name字段建立索引时,系统会为name字段重新建立一棵B+树。这棵B+树的叶子节点并不存放完整的行数据,而是只存放主键值和name字段的值。通过这种方式,我们就能实现对非主键字段(如name)的快速查找。

这就是为其他字段建立索引的基本原理。接下来,我们可以通过示意图更直观地理解这个过程。

大家需要注意的是,这个索引是独立于聚簇索引的另一棵B+树,严格来说,这是一个基于name字段的B+树。在这棵name字段的索引B+树中,叶子节点的数据页仅包含主键和name字段的值,其他字段则不包含。这与聚簇索引的存储结构是有所不同的,但排序规则基本一致。

具体来说,在name字段的索引B+树的叶子节点中,name字段的值是按照大小顺序排序的,且每个数据页中的name值都会遵循这一排序规则。此外,页与页之间的排序也是类似的:下一个数据页中的name字段值会大于上一个数据页中的name字段值。

像聚簇索引一样,name字段的索引B+树也会有多个层级的索引页。这些索引页存储的是指向下层索引页的页号和最小的name字段值,并且依照name字段值进行排序。整体结构和规则与聚簇索引是相似的,只不过这次是针对name字段的排序。

这个结构通过多层次的索引页和按name字段值排序的方式,保证了对于name字段的高效查找。接下来可以通过图示来更加直观地理解这一结构。

对于基于name字段的查询,搜索过程和之前的主键索引类似。首先会从name字段的索引B+树的根节点开始查找,逐层下降,直到找到叶子节点的数据页,其中包含对应name字段的主键值。

然而,这时你只会得到匹配的主键值,而无法直接得到完整的行数据。为了获得完整数据,系统还需要进行“回表”操作。也就是说,通过主键值再去聚簇索引里查找,从根节点开始,一直到达叶子节点的数据页,最终定位到主键对应的完整数据行,这样才可以返回select *查询要求的所有字段。

因此,name字段的索引被称为二级索引,而聚簇索引则是一级索引。二级索引的作用是快速定位到主键,但要获取完整数据时还需要通过聚簇索引。

对于联合索引,例如name+age,原理也类似。系统会构建一颗独立的B+树,其中叶子节点的数据页包含id、name和age字段,且默认按照name字段排序,若name相同,则按age排序。在查询时,系统会根据name+age的联合索引查找,找到主键后,依然需要通过聚簇索引来查找完整数据。

总结一下,InnoDB存储引擎的索引实现原理就是基于B+树的结构,所有的索引(无论是聚簇索引还是二级索引)都是通过B+树的逐层查找来定位数据的。在插入、更新或删除数据时,除了更新数据页外,还会相应地维护索引结构。查询时,合理使用索引可以显著提高查询效率。


8. 插入数据时到底是如何维护好不同索引的B+树的?

之前我们已经详细解析了MySQL数据库的索引结构,大家应该了解了不同类型索引的具体构成,创建方式,以及如何依据这些索引查找数据。

我们将详细解释一下,在插入数据时,如何维护索引的B+树结构。

一开始,当你创建一个表时,系统只会有一个数据页,这个数据页是聚簇索引的一部分,并且目前是空的。

这个时候,插入数据时,数据就会直接放入这个数据页,不需要额外建立其他索引页,如下图所示。

然后,这个初始的数据页其实就是根页,每个数据页内部默认包含一个基于主键的页目录。因此,初期通过主键来进行搜索时没有任何问题,你只需要在唯一的数据页中根据页目录进行查找即可。

随着表中数据量的增加,数据页将会填满。这时,系统会创建一个新的数据页,并将原根页面中的数据复制过去。同时,系统会根据主键值的大小调整数据,将数据分配到两个新的数据页中,确保两个数据页之间的主键值按照顺序排列,即第二个数据页的主键值会大于第一个数据页的主键值,如下图所示。

那么此时,根页去哪儿了呢?

现在,根页已经变成了索引页。这个根页会存放两个数据页的页号以及它们各自的最小主键值。因此,根页的角色已经从数据存储转变为索引功能,引用了这两个数据页,如下图所示。

随着你不断地向表中插入数据,数据页会频繁发生页分裂,产生越来越多的数据页。

这个时候,原本的唯一一个索引页(也就是根页)存储的数据页索引条目逐渐增多,甚至超出了它的存储容量。此时,索引页也会发生分裂,分裂成两个索引页。而根页则会向上升一级,开始引用这两个新的索引页,如下图所示。

接下来,随着数据页的不断增加,根页指向的索引页也会不断分裂,产生更多的索引页。当下层的索引页数量过多时,会导致根页指向的索引页数量过多,这时根页就会进一步分裂,向上升一级,继续引用新的索引页。

这就是在进行增删改操作时,聚簇索引维护的整个过程,其他二级索引的原理也是类似的。


比如,你为name字段建立了索引,开始时数据插入时,数据会同时插入到聚簇索引的唯一数据页和name字段索引B+树的唯一数据页。


随着数据量的增多,name字段的索引B+树中的数据页会发生分裂,整个分裂过程与之前所讲的一样,因此在插入数据时,系统会自动维护各个索引的B+树。


另外补充一下,在name字段的索引B+树中的索引页,除了存放页号和最小的name字段值外,每个索引页还会存放该最小name字段值对应的主键值。这是因为有时候多个索引页指向的下层页号的最小name字段值可能是一样的,此时就需要根据主键值来进一步判断。


比如,假设你插入了一个新的name字段值,这时需要从name字段索引的根页开始,逐层查找,确定新插入的name字段值应该放入哪个数据页。如果不同的索引页指向的下层页号的最小name字段值相同,就必须通过主键值来决定插入的位置。新的name字段值将被插入到主键值较大的数据页。


到这里,大家应该对整个索引的数据结构、基于索引的查询过程以及插入时如何维护B+树有了清晰的理解。


接下来,我们将讨论MySQL在查询语句中如何使用索引,单表查询和多表JOIN的执行原理,MySQL的执行计划以及SQL语句调优。

0

评论区