图文并茂说MySQL索引——入门进阶必备

本篇是我阅读《MySQL是怎样运行的》一书的笔记,我简化了一些概念,删除了一些不容易新手入门理解的内容,在这里分享给大家

文章目录

1.索引介绍

  你一定看到过这样的例子,索引是什么,你要查字典,不可能从第一页翻到最后一页去查找,你会查找拼音或者偏旁部首,找到对应的拼音或者字了之后直接跳转到对应的页查看详细释义,然后一条一条查找,这样会快得多。在这里,我会带大家来看看记录的插入是如何变化的,记录原理是什么。

假设数据表index_demo是这样的:

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

你插入的每条记录的行格式,我们将其简化为如下

图文并茂说MySQL索引——入门进阶必备
  • record_type:记录头信息的一项属性,表示记录的类型,0表示普通记录,1表示目录项记录,2表示Infimum记录(最小记录的上一条记录),3表示Supremum记录(最大记录的下一条记录)。

  • next_record:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,为了方便大家理解,我们都会用箭头来表明下一条记录是谁。

  • 各个列的值:这里只记录在index_demo表中的三个列,分别是c1c2c3

  • 其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。

所以,你插入记录后,结构应该是这样的。

图文并茂说MySQL索引——入门进阶必备

2.当你插入记录,页中有什么变化?

  我们假设每个数据页最多能存放3条记录(实际上一个数据页非常大,可以存放好多条记录)。

  向index_demo插入3条记录

INSERT INTO index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');

变化如下:
图文并茂说MySQL索引——入门进阶必备

可以看到,这些记录组成了单链表,并且按照主键值从小到大的顺序将记录排序。innodb要求插入记录必须这么做。

接着我们在插入一条记录

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

innodb还要求,下一个数据页用户记录的主键值大于上一个页中用户记录的主键值。

而页10最多存放3条数据记录,所以不得不再分配一个新页。

图文并茂说MySQL索引——入门进阶必备

  细心的小伙伴看到此图主键值4的记录在5的后面,别急,下面马上讲到。

  这里分配的页号为什么是28而不是11呢?学过数据结构的人就知道,链表新增结点都是随机分配一块空间,在内存上并不是连续的,这里就假设为28,用来说明这个内存的不连续性。

  不管对页中的记录是增加、删除还是修改,我们必须遵守"下一个数据页用户记录的主键值大于上一个页中用户记录的主键值"这个规则。为了维持这个规则,往往伴随记录的移动,甚至需要为记录分配新页,这个过程也叫页分裂

图文并茂说MySQL索引——入门进阶必备

假设此时你往表中插入了很多条记录,如下

图文并茂说MySQL索引——入门进阶必备

之前说过,查字典不可能从头开始一个个往后查,记录也是一样,不可能一条条顺序往后找,那么innodb是怎么做的呢?

那就是给这些记录一个目录!

图文并茂说MySQL索引——入门进阶必备

文章开头就提到了的record_type属性,它的各个取值代表的意思如下:

0:普通的用户记录
1:目录项记录
2:Infimum记录
3:Supremum记录

可以看到,目录项记录的record_type值是1,而普通用户记录的record_type值是0

  目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB自己添加的隐藏列。

innodb怎么查询呢?

以查找主键为8的记录为例。

  1. 先到存储目录项记录的页(页30)中通过二分法快速定位到用户记录页,因为5< 8< 12,所以定位到对应的记录所在的页就是页28

  2. 再到存储用户记录的页28中根据单链表遍历定位到主键值为8的用户记录。

目录项比较多一页放不下怎么办?那就和用户记录一样进行页分裂。

  我们假设一个目录页最多存放4条目录项记录(真实情况是可以存放很多条),如果此时再插入一条主键值为320的用户记录,那就需要再分配一个新的页去存放目录了。

图文并茂说MySQL索引——入门进阶必备

从图中可以看出,我们插入了一条主键值为320的用户记录之后需要两个新的数据页:

  1. 为存储该用户记录而新生成了页31

  2. 因为原先存储目录项记录的页30的容量已满(我们前边假设只能存储4条目录项记录),所以不得不需要一个新的页32来存放页31对应的目录项。

假如还是查找主键为8的记录,此时innodb该如何查询?

  1. 确定目录项在哪个页。页30的目录项记录的主键值的范围是[1, 320),页32表示的目录项记录的主键值范围[320, +∞),显然主键值为8的记录在页30中。
  2. 通过目录页确定用户记录页。因为5< 8< 12,所以用户记录在页28中。
  3. 在真正用户记录页中定位到具体记录。这个就是单链表遍历即可。

  如果表中的数据确实是很多,会产生很多存储目录项的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?

  那就是为这些存储目录的页再生成一个更高级的目录页,就像是一个多级目录一样。

图文并茂说MySQL索引——入门进阶必备

  我们生成了一个存储更高级目录项的页33,这个页中的两条记录分别代表页30和页32,如果用户记录的主键值在[1, 320)之间,则到页30中查找更详细的目录项记录,如果主键值范围是[320, +∞),就到页32中查找更详细的目录项记录。随着表中记录的增加,这个目录的层级会继续增加。

有人做过这样一个计算,假如叶子结点代表的数据页可以存放100条用户记录,所有目录页可以存放1000条目录项记录,那么:
如果B+树只有1层,也就是只有1个用于存放用户记录的节点,最多能存放100条记录。
如果B+树有2层,最多能存放1000×100=100000条记录。
如果B+树有3层,最多能存放1000×1000×100=100000000条记录。
如果B+树有4层,最多能存放1000×1000×1000×100=100000000000条记录。(一千亿条记录)
一般都不会超过这个数字,也就是说B+树一般不超过4层,可以通过二分法快速定位记录,然后小范围遍历即可。

3.聚集索引

  刚刚上面几节所画的B+树全部都是按照聚集索引(主键值)排序的,可以说B+树本身就是一个索引。聚集索引有以下2个特点

1.使用主键值大小进行记录和页的排序,都是按照主键值升序排列,记录之间是单链表结构,页与页之间是双链表结构。

2.B+树的叶子结点是完整的用户记录,也就是叶子结点存储了所有列的值(包括隐藏列)。

具有这2个特点的B+树称为聚集索引,聚集索引不需要显式使用INDEX语句创建,如下

CREATE INDEX 索引名称 ON 表名(字段);
ALTER TABLE 表名 ADD INDEX 索引名称(字段); 

innodb自动为我们创建聚集索引。

4.非聚集索引(二级索引)

  你肯定遇到过这样的情况,每次查询的时候,where条件并不是按照主键id去筛选条件,比如查询test表中的session_id是"abc"的记录

select * from test where session_id = "abc";

  很显然,除了主键使用默认聚集索引以外,你并没有手动添加其他的索引。那么根据条件去筛选的时候就只能沿着链表依次遍历了。有没有更好的方法呢?

  当然有!我们可以多建几颗B+树啊,不同的B+树可以采用不同的排序规则。比如刚刚的index_demo 表中的c2列的值的大小作为记录和页的排序规则,再建这样一个规则的B+树。

图文并茂说MySQL索引——入门进阶必备

看看这个图,是不是和聚集索引很像?但是还是有点不同。

  1. B+树的叶子结点存储的不是完整的用户记录,没有全部列的信息,只有c2列和主键列这两个列的值。
  2. 目录项记录不再是主键+页号,而是c2列+页号。
  3. 使用记录c2列的大小进行记录和页的排序。不管是用户记录页还是目录页,都是按照c2列的大小升序排列的单链表结构。

创建了非聚集索引后,现在想要查询where c2 = 4的记录的步骤如下:

  1. 确定第一条符合c2 = 4的目录项所在的页。
  2. 确定第一条符合c2 = 4的用户记录所在的页。
  3. 找到符合c2 = 4的第一条用户记录。找到该记录后,由于只存储了c2列和主键c1,所以需要根据该记录的主键信息到聚集索引中查找完整的用户记录(回表)。然后返回非聚集索引的叶子节点处,找到刚才定位到的符合条件的记录,继续往后扫描,因为c2列没有唯一键union约束,可能满足c2 = 4的有多条记录,搜索完本页就跳到下一页第一条记录继续往后扫描,每找到一条满足的记录就进行一次回表操作,重复这个过程,直到下一条记录不满足c2 = 4为止。

5.联合索引

  我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比如说我们想让B+树按照c2c3列的大小进行排序,那么

  1. 先把各个记录和页按照c2列进行排序。
  2. 在记录的c2列相同的情况下,采用c3列进行排序

c2c3列建立的索引,如下:

图文并茂说MySQL索引——入门进阶必备

  每条目录项记录都由c2c3、页号这三个部分组成,各条记录先按照c2列的值进行排序,如果记录的c2列相同,则按照c3列的值进行排序。可以看图中的页50和页55,当c2 = 4的时候,c3按照从小到大的顺序一次是"a""o""u"

  每条用户记录c2c3和主键c1列组成。

6.索引覆盖

  explain查询sql执行计划时,Extra显示Using index时,能够触发索引覆盖。索引覆盖无需回表,需要查询的字段已经都在该B+树上了。索引已经“覆盖了”我们的查询需求,所以称为覆盖索引。

7.非聚集索引一定会查询多次吗?如何避免回表?

  比如有一个联合索引idx_c2_c3(c2, c3)select c3 from 表名 where c2 = 4;就只需要查询一次辅助索引就可以了,因为我需要查询的值正好是索引之一,一棵索引树上就能获取SQL所需的列数据(索引覆盖),无需回表,速度更快。

注意:当你建立c2, c3联合索引之后却看到Extra显示Using index condition; Using filesort,一般是使用了where c2 = "xx" order by c3 desc这样的形式,有人认为Using filesort就是因为c3需要order by才导致外部排序,实际上是你联合索引顺序建反了,index(c2, c3)错误的写成了index(c3, c2),导致B+树优先按照c3排序,c3相同时,才按照c2排序,这样和写的sql意义不符合,具体可以看上面联合索引的图片理解


欢迎一键三连~

有问题请留言,大家一起探讨学习

----------------------Talk is cheap, show me the code-----------------------
上一篇:使用c3动画实现摇铃铛效果


下一篇:docker容器的数据卷