【B+树索引】索引页的结构含有可以快速查询的秘密

索引页的结构含有可以快速查询的秘密

  • 一、记录在页中存储
  • 二、分析完页结构俺能知道啥?
  • 三、B+树是如何进行查询的?
  • 四、聚簇索引和二级索引

一、记录在页中存储

在前文 Innodb存储引擎下的表的逻辑结构 阐述了 Compact 行记录格式。除变长字段、NULL值字段、隐藏字段和数据时,这里的话得重点关注一下头信息,如下所示:

【B+树索引】索引页的结构含有可以快速查询的秘密_第1张图片

  • deleted_flag:删除标记(0:未删除 1:已删除 )。为什么被删除的记录还在页中?或者说,依然在磁盘上?

答:这些被删除的记录之所以没有从磁盘上删除,是因为如果移除了,还需要在磁盘上重新排列剩余的记录,还会带来一定的性能消耗,所以只是打了一个删除的标记就可以避免重拍(即软删除)。然后所有的被删除的记录会组成一个垃圾链表,记录在这个链表中占用的空间被称为可重用空间。之后若是有新的记录插入表中,它们就可以覆盖掉被删除的这些记录占用的存储空间了。

记录是按照行来存储的,但是数据库的读取并不以【行】为单位,否则一次读取(页就是一次 I/O 操作)只能处理一行数据,效率极低。

其实,InnoDB 的数据是按照【数据页】为单位来读写的,也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘中读出来,而是以页为单位,将其整体读入内存。

数据库的 I/O 操作的最小单位是页,InnoDB 数据页的默认大小是 16KB,意味着数据库每次读取都是以 16KB 为单位的,一次最少从磁盘中读取 16K 的内容到内存中,一次最少把内存中的 16K 内容刷新到磁盘中。

一个数据页(索引页)的结构如下:
【B+树索引】索引页的结构含有可以快速查询的秘密_第2张图片下面对上面各个结构作用作阐述:

  • 文件头(File Header):放着页的一些通用信息,有 页类型(索引页、溢出页等等)、前页地址、后页地址、校验和等等。根据前后地址就组成了如下双向的链表(这里注意的地址之间不是连续的
    【B+树索引】索引页的结构含有可以快速查询的秘密_第3张图片

  • 页头(Page Header):放着数据页的专有的一些信息,如:记录数页目录中的槽数、Free Space 在页面中的地址偏移量、当前页所在 B+ 数中的层级等等。

  • 最小和最大记录(Infimum+Supremum):两条虚拟的记录,将记录连接起来有用。其中某记录的 next_record 为负数的话就是指向了最大的记录。

  • 用户记录(User Records):真正存储我们插入的记录,通过 next_record 属性将记录搞成单向链表注意:是按主键大小从小而大排列的。(这也是为什么介意使用MySQL提供的自动递增主键,如果不递增的话,当页已满,但是插入了一条主键在它们之间的记录,那就要对这个页进行分裂,然后找到合适的位置进行插入,导致页分裂会影响性能,所以建议递增处理主键)如下图所示:
    【B+树索引】索引页的结构含有可以快速查询的秘密_第4张图片

  • 空闲空间(Free Space):页中尚未使用的部分。当 Free Space 被 User Records 替代完后,这个页就意味着用完了。

  • 页目录(Page Directory):实时上这个页内单链表的记录是被分组了的,各个组的索引值在页目录中有记录,这个索引值是指组中最后一条记录的地址偏移量(就是该记录真实的数据与页面中第0个字节之间的距离)。这个索引值存储在页目录中,在页目录中它们称为槽(Slot),每个槽占有 2 字节。页目录就是由多个槽组成的。(其中 n_owned 属性表示的是组内有几条记录,当然最后一条记录就是指组内主键值最大的那个条记录
    【B+树索引】索引页的结构含有可以快速查询的秘密_第5张图片

  • 文件尾(File Tailer):用于校验页是否完整,和文件头的校验和去匹配。

二、分析完页结构俺能知道啥?

  • 用户的记录存在 User Records 中,以单向链表存储记录,切是以主键大小从前往后存储的

  • 页目录创建的过程如下:

    • 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为 “已删除” 的记录;
    • 每个记录的最后一条记录就是组内主键值最大的那条,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned1 字段。组成员的话这个属性字段值是0.
    • 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量页被称为槽(Slot),每个槽相当于指针指向了不同组的最后一个记录。
  • 查找记录过程根据主键值从主目录中找到记录在哪个槽中(哪个记录分组),定位到槽后,再遍历槽内所有记录就可以找到记录了,无需最小记录开始遍历整个页中记录链表。

  • 槽内的记录是有规定的,并不是随便几条就几条,那样的话查找记录时间复杂度不就O(n)了:第一个分组中的记录只能有 1 条记录,即Infimum,最后一个分组中的记录条数的范围只能在 1-8 条之间 即Supremum;剩下的分组中记录条数范围只能在 4-8 条之间。

三、B+树是如何进行查询的?

上面提到的记录检索是针对单个数据页,但是单个数据页的记录是有限的,且主键值是有序的,所以通过对所有记录进行分组,然后将组号(槽号)存储到页目录,使其起到索引作用,通过二分查找的方法快速检索到记录在哪个分组,来降低检索的时间复杂度。

但是,当我们需要存储大量的记录时,就需要多个数据页,这时我们就需要考虑如何建立合适的索引,才能方便定位记录所在页。

为了解决这个问题,InnoDB 采用了 B+ 树作为索引,InnoDB 里的 B+ 树中的每个节点都是一个数据页。只是说叶子节点存放的数据是整个数据,而非叶子节点仅用来存放目录作为索引,也就是页码(页地址),可以快速定位到那个页然后再根据上面页的查询记录的过程进行查询

这里的话还得阐述头信息的一个属性 record_type:0表示普通记录、1表示非叶子节点,也就是B+树非叶子节点的目录项记录、2表示 Infimum 记录、3 表示 Supremum 记录。

InnoDB 里的 B+ 树的结构示意图如下(每个节点都是一个索引页,也就是说都有头信息、隐藏字段…):
【B+树索引】索引页的结构含有可以快速查询的秘密_第6张图片
通过上图,可以看出 B+ 树的特点:

  • 每个索引页内部的记录仍然是按主键递增进行排单向链表的,非叶子节点页也是的,其主键值对应的是索引页对应的页中记录最小的主键值。(就拿聚簇索引为例吧,不然我不好解释)
  • 只有叶子节点(最底层的节点)才存放了数据,非叶子节点(其他层节点)仅用来存放目录项作为索引,也就是定位到下一层的页
  • 非叶子节点分为不同层次,通过分层来降低每一层的搜索量,当然最高就四层。
  • 所有节点按照索引键大小排序,构成一个双向链表,便于范围查询,根据文件头提供的信息(File Header)。
  • 叶子节点 record_type 属性值为0,非叶子节点即目录项节点 record_type 属性值为1.

看看上图的 B+ 树 是如何快速查找主键为 6 的记录的:

  • 从根节点开始,通过二分法快速定位到符合页内范围包括查询值的页,因为查询的住兼职是6,在[1,7)之间,所以到页30中查询。
  • 由于页30的记录 record_type 是1,也就是目录项记录,所以继续通过二分法快速定位到查询值的页,主键值大于 5,所以定位到页 16.
  • 由于页16的记录record_type是0,说明是用户记录,通过二分法定位到哪个槽后,遍历槽内所有记录,然后找到了主键为 6 的记录。

四、聚簇索引和二级索引

上面说的 B+ 树其实就是我们提到的索引。树的叶子节点存放着完整的用户记录,而非叶子节点记录着下层的页的位置。

但其实在 InnoDB 中,索引分为聚簇索引和非聚簇索引(二级索引),它们区别就在于叶子节点存放的是什么数据:

  • 聚簇索引的叶子结点存放的是实际数据,所有完整的用户记录都存放在聚簇索引的叶子节点中;
  • 二级索引的叶子节点存放的是主键值,而不是实际数据。也就是说它查询实际数据数据还需要回表,如果只是查询主键,会产生索引覆盖即不需要回表。

因为表的数据都是存放在聚簇索引的叶子结点中的,所以 InnoDB 存储引擎一定为表创建一个聚簇索引,且由于数据物理上只会保存一份,所以聚簇索引只能用一个。(其实原本是只有一个根节点,然后不断添加数据,一页一页加到后面就成了我们看到的B+树索引了,这在下面一篇博客阐述注意事项会解释)

InnoDB 在创建聚簇索引时,会根据不同的场景选择不同的列作为索引:

  • 如果有主键,默认会使用主键作为聚簇索引的索引键;
  • 如果没有主见,就选择一个不包含NULL值的唯一列作为聚簇索引的索引键;
  • 如果以上俩都不满足,InnoDB 将自动生成一个隐式自增 id 列(6字节)作为聚簇索引的索引键。

一张表只能有一个聚簇索引,为了实现非主键字段的快速搜索,就引出了二级索引(非聚簇索引/辅助索引),它也是利用了 B+ 树的数据结构,但是二级索引的叶子节点存放的是主键值,不是实际数据,查找实际数据还需要回表。

二级索引的 B+ 树如下图,数据部分为主键值:
【B+树索引】索引页的结构含有可以快速查询的秘密_第7张图片

如果某个查询语句使用了二级索引,但是查询的数据不是主键值,这时在二级索引找到主键值后,需要去聚簇索引中获得数据行,这个过程就叫作「回表」,也就是说要查两个 B+ 树才能查到数据。不过,当查询的数据是主键值时,因为只在二级索引就能查询到,不用再去聚簇索引查,这个过程就叫作「索引覆盖」,也就是只需要查一个 B+ 树就能找到数据。

关于索引的注意事项、索引的使用、索引失效、索引优化等等,后续博客会更新滴~~~

参考文献:
《MYSQL是怎样运行的》
从数据页的角度看 B+ 树
一文了解数据页

你可能感兴趣的:(MySQL进阶,b树,数据库,数据结构)