图文并茂说MySQL索引——入门进阶必备
Posted 砖业洋__
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图文并茂说MySQL索引——入门进阶必备相关的知识,希望对你有一定的参考价值。
本篇是我阅读《mysql是怎样运行的》一书的笔记,我简化了一些概念,删除了一些不容易新手入门理解的内容,在这里分享给大家
1.索引介绍
你一定看到过这样的例子,索引是什么,你要查字典,不可能从第一页翻到最后一页去查找,你会查找拼音或者偏旁部首,找到对应的拼音或者字了之后直接跳转到对应的页查看详细释义,然后一条一条查找,这样会快得多。在这里,我会带大家来看看记录的插入是如何变化的,记录原理是什么。
假设数据表index_demo
是这样的:
CREATE TABLE index_demo(
c1 INT,
c2 INT,
c3 CHAR(1),
PRIMARY KEY(c1)
)charset=utf8mb4;
你插入的每条记录的行格式,我们将其简化为如下
-
record_type
:记录头信息的一项属性,表示记录的类型,0
表示普通记录,1
表示目录项记录,2
表示Infimum
记录(最小记录的上一条记录),3
表示Supremum
记录(最大记录的下一条记录)。 -
next_record
:记录头信息的一项属性,表示下一条地址相对于本条记录的地址偏移量,为了方便大家理解,我们都会用箭头来表明下一条记录是谁。 -
各个列的值:这里只记录在
index_demo
表中的三个列,分别是c1
、c2
和c3
。 -
其他信息:除了上述3种信息以外的所有信息,包括其他隐藏列的值以及记录的额外信息。
所以,你插入记录后,结构应该是这样的。
2.当你插入记录,页中有什么变化?
我们假设每个数据页最多能存放3
条记录(实际上一个数据页非常大,可以存放好多条记录)。
向index_demo
插入3
条记录
INSERT INTO index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');
变化如下:
可以看到,这些记录组成了单链表,并且按照主键值从小到大的顺序将记录排序。innodb
要求插入记录必须这么做。
接着我们在插入一条记录
INSERT INTO index_demo VALUES(4, 4, 'a');
innodb
还要求,下一个数据页用户记录的主键值大于上一个页中用户记录的主键值。
而页10
最多存放3
条数据记录,所以不得不再分配一个新页。
细心的小伙伴看到此图主键值4
的记录在5
的后面,别急,下面马上讲到。
这里分配的页号为什么是28
而不是11
呢?学过数据结构的人就知道,链表新增结点都是随机分配一块空间,在内存上并不是连续的,这里就假设为28
,用来说明这个内存的不连续性。
不管对页中的记录是增加、删除还是修改,我们必须遵守"下一个数据页用户记录的主键值大于上一个页中用户记录的主键值"这个规则。为了维持这个规则,往往伴随记录的移动,甚至需要为记录分配新页,这个过程也叫页分裂。
假设此时你往表中插入了很多条记录,如下
之前说过,查字典不可能从头开始一个个往后查,记录也是一样,不可能一条条顺序往后找,那么innodb
是怎么做的呢?
那就是给这些记录一个目录!
文章开头就提到了的record_type
属性,它的各个取值代表的意思如下:
0:普通的用户记录
1:目录项记录
2:Infimum
记录
3:Supremum
记录
可以看到,目录项记录的record_type
值是1
,而普通用户记录的record_type
值是0
。
目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户自己定义的,可能包含很多列,另外还有InnoDB
自己添加的隐藏列。
那innodb
怎么查询呢?
以查找主键为8
的记录为例。
-
先到存储目录项记录的页(页
30
)中通过二分法快速定位到用户记录页,因为5< 8< 12
,所以定位到对应的记录所在的页就是页28
。 -
再到存储用户记录的页
28
中根据单链表遍历定位到主键值为8
的用户记录。
目录项比较多一页放不下怎么办?那就和用户记录一样进行页分裂。
我们假设一个目录页最多存放4
条目录项记录(真实情况是可以存放很多条),如果此时再插入一条主键值为320
的用户记录,那就需要再分配一个新的页去存放目录了。
从图中可以看出,我们插入了一条主键值为320
的用户记录之后需要两个新的数据页:
-
为存储该用户记录而新生成了页
31
。 -
因为原先存储目录项记录的页
30
的容量已满(我们前边假设只能存储4
条目录项记录),所以不得不需要一个新的页32
来存放页31
对应的目录项。
假如还是查找主键为8
的记录,此时innodb
该如何查询?
- 确定目录项在哪个页。页
30
的目录项记录的主键值的范围是[1, 320)
,页32
表示的目录项记录的主键值范围[320, +∞)
,显然主键值为8
的记录在页30
中。 - 通过目录页确定用户记录页。因为
5< 8< 12
,所以用户记录在页28
中。 - 在真正用户记录页中定位到具体记录。这个就是单链表遍历即可。
如果表中的数据确实是很多,会产生很多存储目录项的页,那我们怎么根据主键值快速定位一个存储目录项记录的页呢?
那就是为这些存储目录的页再生成一个更高级的目录页,就像是一个多级目录一样。
我们生成了一个存储更高级目录项的页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+
树。
看看这个图,是不是和聚集索引很像?但是还是有点不同。
B+
树的叶子结点存储的不是完整的用户记录,没有全部列的信息,只有c2
列和主键列这两个列的值。- 目录项记录不再是主键+页号,而是
c2
列+页号。 - 使用记录
c2
列的大小进行记录和页的排序。不管是用户记录页还是目录页,都是按照c2
列的大小升序排列的单链表结构。
创建了非聚集索引后,现在想要查询where c2 = 4
的记录的步骤如下:
- 确定第一条符合
c2 = 4
的目录项所在的页。 - 确定第一条符合
c2 = 4
的用户记录所在的页。 - 找到符合
c2 = 4
的第一条用户记录。找到该记录后,由于只存储了c2
列和主键c1
,所以需要根据该记录的主键信息到聚集索引中查找完整的用户记录(回表)。然后返回非聚集索引的叶子节点处,找到刚才定位到的符合条件的记录,继续往后扫描,因为c2
列没有唯一键union
约束,可能满足c2 = 4
的有多条记录,搜索完本页就跳到下一页第一条记录继续往后扫描,每找到一条满足的记录就进行一次回表操作,重复这个过程,直到下一条记录不满足c2 = 4
为止。
5.联合索引
我们也可以同时以多个列的大小作为排序规则,也就是同时为多个列建立索引,比如说我们想让B+
树按照c2
和c3
列的大小进行排序,那么
- 先把各个记录和页按照
c2
列进行排序。 - 在记录的
c2
列相同的情况下,采用c3
列进行排序
为c2
和c3
列建立的索引,如下:
每条目录项记录都由c2
、c3
、页号这三个部分组成,各条记录先按照c2
列的值进行排序,如果记录的c2
列相同,则按照c3
列的值进行排序。可以看图中的页50
和页55
,当c2 = 4
的时候,c3
按照从小到大的顺序一次是"a"
,"o"
,"u"
,
每条用户记录由c2
、c3
和主键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-----------------------
以上是关于图文并茂说MySQL索引——入门进阶必备的主要内容,如果未能解决你的问题,请参考以下文章