MySQL(InnoDB剖析):---B+树索引(聚集索引与非聚集索引(辅助索引))、B+树索引的分裂

小伙伴们大家好!今天是大年三十,给大家拜个早年!在此小弟祝各位大哥们与家人团团圆圆,和和睦睦,新的一年身体健康,工作顺利!

一、B+树索引概述

  • B+树索引的本质就是B+树在数据库中的实现。但是B+索引在数据库中有一个特点就是高扇出性,因此在数据库中B+树的高度一般都在2~4层,也就是说查找某一键值的行记录最多只需要2~4次IO。因为当前一般的机械磁盘每秒至少可以做100次IO,2~4次IO意味着查询时间只需0.02~0.04秒

B+树的大致工作原理

  • 先抛开各种实现细节,来说一说B+树索引的大致工作原理
  • 工作原理如下
    • 假设现在是主键索引表,一个表中有0、1、2、3、4、5、6、7、8八个主键
    • 那么在B+树中,其叶子节点存储的是这些主键
    • 当我们select * from table where id >1 and id <7的时候,那么会先从B+树的根节点开始向下查找,查找到1这个节点之后,由于底层节点之间是链表形式组织的,因此主键向右进行查找,从而查找到2、3、4、5、6这几个节点
    • insert、update、delete这些都是相同的原理

InnoDB和MyIASM的B+树之间有什么差别

  • InnoDB和MyIASM都支持B+树索引,那么它们之间的区别是什么呢?
    • InnoDB其叶子节点存储不仅存储着主键的值,并且还存储着该主键对应的行数据。因此,其每个叶子节点=主键+整行数据值
    • MyIASM其叶子节点也是存储着主键的值,但是其不存储该主键对应的行数据,其存储的是指向该行数据对应的地址。当我们查找到该主键值,再通过该指针查找到对应地址上的值

面试题:MyIASM与InnoDB通过B+数索引操作数据,哪一个更快?

  • 规则为
    • 当操作的数据量不多时,可能两者没什么差别
    • 当操作的数据量较大时,那么InnoDB比MyIASM快
  • 解释如下
    • InnoDB从磁盘读取数据在内存中构造一棵B+树,由于其B+树叶子节点存储的都是数据的值,因此其数据直接存储在内存中
    • MyIASM从磁盘读取数据在内存中构造一棵B+树,由于其B+树叶子节点只存储数据对应的指针,不存储值,因此其不会将数据读取都内存中
    • 所以当数据量不多时,两者没什么区别:因为数据量小,InnoDB直接从内存中取数据,MyIASM通过指针去磁盘中查找数据,效率差别不大
    • 当操作的数据量较大时,InnoDB直接从内存中取数据,那么速度较快,但是MyIASM需要不断的通过指针去磁盘中取数据,从而导致速度较慢

B+树索引分类

  • 分为:
    • 聚集索引(clustered index)按照每张表的主键构造一棵B+树,一个表只能有一个聚集索引
    • 辅助索引(secondary index)根据非主键构造B+树,一个表可以有多个辅助索引
  • 不管是聚集还是辅助索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据
  • 聚集索引与辅助索引不同的是,叶子节点存放的是否为一整行的信息

二、聚集索引

聚集索引结构

  • 前面介绍过,InnoDB是索引组织表,即表中数据按照主键顺序存放
  • 而聚集索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页。聚集索引的这个特性决定了索引组织表中的数据也是索引的一部分。同B+树结构一样,每个数据页都通过一个双向链表进行链接
  • 由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引
  • 在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上直接找到数据。此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。查询优化器能够快速发现某一段范围的数据页需要扫描

B+树结构分析案例

  • 下面创建一个表,让每个页只能存放两个行记录
create table t(
    a int not null,
    b varchar(8000),
    primary key(a)
)engine=innodb;
  • 插入数据插入的列b长度为7000,因此可以人为的使目前每个页只能存放两个行
insert into t select 1,repeat('a',7000);
insert into t select 2,repeat('a',7000);
insert into t select 3,repeat('a',7000);
insert into t select 4,repeat('a',7000);
  • 使用py_innodb+page_info工具分析表空间,可得
    • page level为0000的是数据页,前面的章节对数据页进行分析了,现在我们不重点关注这一部分
    • page level为0001的页,当前B+树高度为2,因此这个页是B+树的根

  • 通过hexdump工具观察说是因的根页中所存放的数据,然后通过页尾的Page Directory来分析此页
    • 从00 63可以知道该页中行开始的位置
    • 接着通过Recorder Header来分析
      • 0xc063开始的值为69 6e 66 69 6d 75 6d 00,就代表infimum为行记录
      • 之前的5字节01 00 02 00 1b就是Recorder Header,分析第4位到第8位的值1代表该行记录中只有一个记录(需要记住的是,InnoDB的Page Directory是稀疏的),即infimum记录本身
      • 通过Recorder Header最后两个字节00 1b来判断下一条记录的位置,即c063+1b=c07e,读取键值可得80 00 00 01,这就是主键为1的键值(表定义时int是无符号的,因此二进制是0x80 00 00 01,而不是0x0001)
    • 80 00 00 01后的值00 00 00 04代表指向数据页的页号
    • 同样的方式可以找到80 00 00 02和80 00 00 04这两个键值以及它们指向的数据页

  • 通过以上对非数据页节点的分析,可以发现数据页上存放的是完整的每行的记录,而在非数据页的索引页中,存放的仅仅是键值及指向数据页的偏移量,而不是一个完整的行记录。因此这棵聚集索引数的构造大致如下图所示

聚集索引的存储并不是物理上连续的

  • 许多数据库的文档和网上的博客都说聚集索引按照顺序物理地存储数据。通过上图可以看出,如果聚集索引必须按照特定顺序存放物理记录,则维护成本显得非常只改
  • 所以聚集索引的存储并不是物理上连续的,而是逻辑上连续的
  • 这其中有两点
    • 一是前面说过的页通过双向链表连接,页按照主键的顺序排序
    • 另一点就是每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储

聚集索引的“快速查询”优点

  • 聚集索引的另一个好处就是,它对于主键的排序查找和范围查找速度非常快,叶子节点的数据就是用户所要查询的数据
  • 如用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引是双向链表的,用户可以快速找到最后一个数据页,并取出10条记录。若用EXPLAIN进行分析,可得:
    • 这里虽然使用order by对记录进行排序,但是在实际过程中并没有进行所谓的filesort操作,而这就是因为聚集索引的特点

  • 另一个是范围查询即如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。又如:
    • 执行explain得到了MySQL数据库的执行计划(execute plan),并且在rows列中给出了一个查询结果的预估返回行数。要注意的是,rows代表是一个预估值,不是确切的值,如果实际执行这句SQL的查询,可以看到实际上只有9946行记录

三、辅助索引(非聚集索引)

辅助索引结构

  • 辅助索引(secondary index)也称为非聚集索引,叶子节点并不包含行记录的全部数据
  • 叶子节点除了包含键值以外每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉InnoDB存储引擎哪里可以找到与所以相对应的行数据。由于InnoDB存储引擎表是索引组织表,因此InnoDB存储引擎的辅助索引的书签就是相应行数据的聚集索引键
  • 下图显示了InnoDB存储引擎中辅助索引与聚集索引的关系:

工作原理

  • 辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引
  • 当通过辅助索引来寻找数据时,InnoDB存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录
  • 例如如果在一棵高度为3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页,因此一共需要6次逻辑IO访问以得到最终的一个数据页

辅助索引结构分析

  • 还是以上面的表t为例,然后再添加一个列
create table t(
    a int not null,
    b varchar(8000),
    primary key(a)
)engine=innodb;
alter table t add c int not null;
  • 为每个行更新一下新添加列c的值
  • update t set c=0-a;
  • 对列c创建非聚集索引
alter table t add key idx_c(c);
  • 查看一下当前的索引

  • 查看一下当前表的数据

  • 利用py_innodb_page_info工具分析表空间,可得:

  • 与上面聚集索引相比较,此次多出了一个页。分析page offset为4的页,该页即为非聚集索引所在页,通过工具hexdump分析可得:

由于只有4行数据,并且列c只有4字节,因此在一个非聚集索引页中即可完成,整理分析可得如下图所示的关系,下图显示了表t辅助索引idx_c和聚集索引的关系:

  • 可以看到辅助索引的叶子节点包含了列c的值和主键的值
  • 因为这里我们特意将键值设为负值,所以会发现-1以7f ff ff ff的方式进行内部存储
  • 7(0111)最高位为0,代表负值,实际的值应该取反后加1,即得-1

四、B+树索引的分裂

  • B+树索引的分裂与B+数的插入操作不同,B+树索引页的分裂并不总是从页的中间记录开始,这样可能会导致页空间的浪费

演示说明

  • 插入是根据自增顺序进行的,若这时插入了10条记录

  • 之后如果还要插入记录并且需要分页操作,那么会将记录5作为分裂点,分裂之后得到下面两个页

  • 我们知道插入时顺序插入的,因此P1这个页中将不会再有记录被插入,因此导致空间浪费,而P2又会再次进行分裂
  • InnoDB存储引擎的Page Header有以下几个部分来保存插入的顺序信息
    • PAGE_LAST_INSERT

    • PAGE_DIRECTION

    • PAGE_B_DIRECTION

  • 通过这些信息,InnoDB存储引擎可以决定是向右还是向左进行分裂,同时决定将分裂点记录为哪一个
    • 若插入是随机的,则取页的中间记录作为分裂点的记录,这和之前介绍的相同
    • 若往同一方向插入的记录数量为5,并且目前已经定位(cursor)到的记录(InnoDB时,首先需要进行定位,定位到的记录为待插入记录的前一条记录)之后还有3条记录,则分裂点的记录为定位到的记录后的第三条记录,否则分裂点记录就是待插入的记录

演示案例

  • 现在看看一个向右分裂的例子,并且定位到的记录之后还有3条记录,则分裂点记录如下图所示:

  • 上图向右分裂且定位到的记录之后还有3条记录,split record为分裂点记录、最终向右分裂得到下图所示的情况

演示案例

  • 在上面演示案例中,分裂点就为插入记录本身,向右分裂后仅插入记录本身,这在自增插入时是普遍存在的一种情况

猜你喜欢

转载自blog.csdn.net/m0_46405589/article/details/113788256