Mysql快速查询的秘籍--B+树索引的理解

一、没有索引时的查找

SELECT [列名列表] FROM 表名 WHERE 列名 = xxx;

1. 在一个页中查找

假设目前表中的记录比较少,所有的记录都可以被存放到一个页中。可以根据搜索条件的不同分为两种情况:

  • 以主键为搜索条件
  • 以其他列作为搜索条件

2. 在很多页中查找

大部分情况下我们表中存放的记录都是非常多的,需要好多的数据页来存储这些记录。在很多页中查找记录的话可以分为两个步骤:

  1. 定位到记录所在的页。
  2. 从所在的页内中查找相应的记录。

在没有索引的情况下,不论是根据主键列或者其他列的值进行查找,由于我们并不能快速的定位到记录所在的页,所以只能从第一个页沿着双向链表一直往下找,在每一个页中再去查找指定的记录。如果一个表中的记录非常的多,这种查找效率是很低的。

二、索引

先建立一个表:

CREATE TABLE index_demo(
  c1 INT,
  c2 INT,
  c3 CHAR(1),
  PRIMARY KEY(c1)
 ) ROW_FORMAT = Compact;

这个表的行格式如下:
在这里插入图片描述
record_type :表示记录的类型。0表示普通记录、1表示目录项记录 、2表示最小记录、 3表示最大记录

next_record :表示下一条地址相对于本条记录的地址偏移量。(为了方便理解,之后的图示都会用箭头来表明下一条记录是谁。)

把一些记录放到页里边的就是:
在这里插入图片描述

1. 简单的索引方案

因为各个页中的记录并没有规律,我们并不知道我们的搜索条件匹配哪些页中的记录,所以 不得不 依次遍历所有的数据页。

如果我们想快速的定位到需要查找的记录在哪些数据页,可以为所在的数据页再建立一个别的目录。建立的这个目录主要满足:

  • 下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值(假设:我们的每个数据页最多能存放3条记录)
INSERT INTO index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');
Query OK, 3 rows affected (0.01 sec)

在这里插入图片描述
此时我们再来插入一条记录:

INSERT INTO index_demo VALUES(4, 4, 'a');

因为页10最多只能放3条记录,所以我们不得不再分配一个新页:
在这里插入图片描述

新分配的数据页编号可能并不是连续的,也就是说我们使用的这些页在存储空间里可能并不挨着。它们只是通过维护着上一个页和下一个页的编号而建立了链表关系。

页10中用户记录最大的主键值是 5 ,而 页28 中有一条记录的主键值是 4 ,因为 5>4 ,所以这就不符合下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值的要求,所以在插入主键值为 4 的记录的时候需要伴随着一次记录移动,也就是把主值为 5 的记录移动到 页28 中,然后再把主键值为 4 的记录插入到页10中。

这个过程称为页分裂:
在这里插入图片描述

  • 给所有的页建立一个目录项
    由于数据页的编号可能并不是连续的,所以在向 index_demo 表中插入许多条记录后,可能是这样的效果:在这里插入图片描述
    想从这么多页中根据主键值快速定位某些记录所在的页,我们需要给它们做个目录,每个页对应一个目录项。在这里插入图片描述
    我们只需要把几个目录项在物理存储器上连续存储,比如把他们放到一个数组里,就可以实现根据主键值快速查找某条记录的功能了。

针对数据页做的简易目录就搞定了。这个目录有一个别名,称为索引

2. InnoDB中的索引方案

上边之所以称为一个简易的索引方案,是因为我们为了在根据主键值进行查找时使用二分法快速定位具体的目录项而假设所有目录项都可以在物理存储器上连续存储,但是这样做有几个问题:

  • InnoDB 是使用页来作为管理存储空间的基本单位,也就是最多能保证 16KB 的连续存储空间,而随着表中记录数量的增多,需要非常大的连续的存储空间才能把所有的目录项都放下,这对记录数量非常多的表是不现
    实的。
  • 我们时常会对记录进行增删,假设我们把页28中的记录都删除了,页28也就没有存在的必要了,那意味着目录项2也就没有存在的必要了,这就需要把 目录项2后的目录项都向前移动一下。

所以我们可以复用了之前存储用户记录的数据页来存储目录项,为了和用户记录做一下区分,我们把这些用来表示目录项的记录称为目录项记录

InnoDB怎么区分一条记录是普通的用户记录还是目录项记录呢?

  • 通过记录头信息里的record_type属性
    0 :普通的用户记录 1 :目录项记录 2 :最小记录 3 :最大记录
    在这里插入图片描述

目录项记录和普通的用户记录的不同点:

  1. 目录项记录的 record_type 值是1,而普通用户记录的 record_type 值是0。
  2. 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。
  3. 记录头信息有一个min_rec_mask属性,只有在存储目录项记录的页中的主键值最小的目录项记录 的 min_rec_mask 值为 1 ,其他别的记录的 min_rec_mask 值都是 0 。

所以,根据某个主键值去查找记录的步骤就可以大致拆分成下边两步:

  1. 先到存储目录项记录的页,也就是页 30中通过二分法快速定位到对应目录项,因为 12 < 20 < 209 ,所以定位到对应的记录所在的页就是页9。
  2. 再到存储用户记录的页9中根据二分法快速定位到主键值为20的用户记录。

问:虽然说目录项记录中只存储主键值和对应的页号,比用户记录需要的存储空间小多了,但是一个页只有 16KB 大小,能存放的目录项记录也是有限的,那如果表中的数据太多,以至于一个数据页不足以存放所有的目录项记录,该咋办呢?
答:再多添加一个存储目录项记录的页

我们假设一个存储目录项记录的页最多只能存放4条目录项记录,则:在这里插入图片描述
现在因为存储目录项记录的页不止一个,所以如果我们想根据主键值查找一条用户记录大致需要3个步骤:

  1. 确定目录项记录页。
  2. 通过目录项记录页确定用户记录真实所在的页。
  3. 在真实存储用户记录的页中定位到具体的记录。

问:第1步中我们需要定位存储目录项记录的页,但是这些页在存储空间中也可能不挨着,如果我们表中的数据非常多则会产生很多存储目录项记录的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?
答:为这些存储目录项记录的页再生成一个更高级的目录。
在这里插入图片描述
我们发现,这个图就是一颗B+树。

不论是存放用户记录的数据页,还是存放目录项记录的数据页,我们都把它们存放到 B+ 树这个数据结构中了,所以我们也称这些数据页为节点

从图中可以看出来,我们的实际用户记录其实都存放在B+树的最底层的节点上,这些节点也被称为叶子节点 ,其余用来存放目录项的节点称为非叶子节点,其中 B+ 树最上边的那个节点也称为根节点 。

  • InnoDB规定最下边的那层,也就是存放用户记录的那层为第 0 层,之后依次往上加。
  • 一般情况下,我们用到的B+树都不会超过4层。
  • 通过主键值去查找某条记录最多只需要做4个页面内的查找(查找3个目录项页和一个用户记录页),又因为在每个页面内有所Page Directory 页目录),所以在页面内也可以通过二分法实现快速定位记录。

1. 聚簇索引

之前介绍的 B+ 树本身就是一个目录,或者说本身就是一个索引。它有两个特点:

  1. 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
  • 页内的记录是按照主键的大小顺序排成一个单向链表。
  • 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
  • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
  1. B+ 树的叶子节点存储的是完整的用户记录。
  • 完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)

我们把具有这两种特性的 B+ 树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点处。这种聚簇索引并不需要我们在 MySQL 语句中显式的使用 INDEX 语句去创建,InnoDB 存储引擎会自动的为我们创建聚簇索引。

在InnoDB存储引擎中,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据,数据即索引。

2. 二级索引

聚簇索引只能在搜索条件是主键值时才能发挥作用,因为 B+ 树中的数据都是按照
主键进行排序的。那如果我们想以别的列作为搜索条件该怎么办呢?

我们可以多建几棵B+树,不同的B+树中的数据采用不同的排序规则。比方说我们用c2列的大小作为数据页、页中记录的排序规则,再建一棵B+树,如图所示:
在这里插入图片描述
这个 B+ 树与上边介绍的聚簇索引有几处不同:

  • 使用记录c2列的大小进行记录和页的排序,这包括三个方面的含义:
  1. 页内的记录是按照c2列的大小顺序排成一个单向链表。
  2. 各个存放用户记录的页也是根据页中记录的c2列大小顺序排成一个双向链表。
  3. 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的c2列大小顺序排成一个双向链表。
  • B+ 树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值。
  • 目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配。

在查找用户记录时,由于叶子结点只存储了c2和主键列,所以我们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录,这个过程叫做回表

问:为什么不直接把完整的用户记录放在叶子结点呢?
答:虽然在叶子结点存放完整的用户记录就不用回表了,但是这样做就相当于每建立一棵B+树都要把所有用户记录拷贝一遍,太浪费存储空间了。因此这种按照非主键列建立的B+树需要一次回表操作才可以定位到完整的用户记录,所以这种B+树也被称为二级索引或者辅助索引。

由于我们使用的是c2列的大小作为B+树的排序规则,所以我们也称这个B+树为为c2列建立的索引

3. 联合索引

我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比方说我们想让B+树按照c2和c3列的大小进行排序,这个包含两层含义:

  • 先把各个记录和页按照c2列进行排序。
  • 在记录的c2列相同的情况下,采用c3列进行排序
    在这里插入图片描述
  • 各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。
  • B+树叶子节点处的用户记录由c2 、 c3和主键c1列组成。

以c2和c3列的大小为排序规则建立的B+树称为联合索引,本质上也是一个二级索引。它的意思与分别为c2和c3列分别建立索引的表述是不同的。

  • 建立联合索引只会建立如上图一样的1棵B+树。
  • 为c2和c3列分别建立索引会分别以c2和c3列的大小为排序规则建立2棵B+树。

三、注意事项

前面介绍B+树索引的时候,为了理解方便,先把存储用户记录的叶子节点都画出来,然后接着画存储目录项记录的内节点,但实际上 B+ 树的形成过程是这样的:

  1. 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个 B+ 树索引对应的根节点中既没有用户记录,也没有目录项记录。
  2. 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。
  3. 当根节点中的可用空间用完时继续插入记录,此时会将 根节点 中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如 页b 。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页 中,而根节点便升级为存储目录项记录的页

一个B+树索引的根节点自诞生之日起,便不会再移动。这样只要我们对某个表建立一个索引,那么它的根节点的页号便会被记录到某个地方,然后凡是InnoDB 存储引擎需要用到这个索引的时候,都会从那个固定的地方取出根节点的页号,从而来访问这个索引。

B+ 树索引的内节点中目录项记录的内容是索引列 + 页号的搭配,但是这个搭配对于二级索引来说有点儿不严谨。我们需要保证在B+树的同一层内节点的目录项记录除页号这个字段以外是唯一的。

所以对于二级索引的内节点的目录项记录的内容实际上是由三个部分构成的:

  • 索引列的值
  • 主键值
  • 页号

在InnoDB中索引即数据,也就是聚簇索引的那棵B+树的叶子节点中已经把所有完整的用户记录都包含了,而MyISAM的索引方案虽然也使用树形结构,但是却将索引和数据分开存储。

四、MySQL中创建和删除索引

InnoDB 和 MyISAM 会自动为主键或者声明为UNIQUE的列去自动建立B+树索引,但是如果我们想为其他的列建立索引就需要我们显式的去指明。

#创建
CREATE TALBE 表名 (
  各种列的信息 ··· ,
  [KEY|INDEX] 索引名 (需要被索引的单个列或多个列)
)

ALTER TABLE 表名 ADD [INDEX|KEY] 索引名 (需要被索引的单个列或多个列);

ALTER TABLE 表名 DROP [INDEX|KEY] 索引名;

#删除
ALTER TABLE 表名 DROP INDEX 索引名;

比方说我们想在创建index_demo表的时候就为 c2 和 c3 列添加一个联合索引 ,可以这么写建表语句:

CREATE TABLE index_demo(
	c1 INT,
	c2 INT,
	c3 CHAR(1),
	PRIMARY KEY(c1),
	INDEX idx_c2_c3 (c2, c3)
);

猜你喜欢

转载自blog.csdn.net/myjess/article/details/115550686