关于索引,这是一个很是重要的知识点,一样,在面试的时候也会被常常的问到;mysql
本文描述了索引的结构,介绍了InnoDB的索引方案等知识点,感兴趣的能够看一下;面试
本文参考文章:MySQL的索引sql
在上篇文章中咱们说到 InnoDB的数据页结构 ,了解到了InnoDB
数据页的 7 个组成部分,知道了各个数据页能够组成一个双向链表
,而每一个数据页中的记录又能够组成一个单向链表
(按照大小排序),每一个数据页都会为存储在它里边儿的记录生成一个页目录
,在经过主键查找某条记录的时候能够在页目录
中使用二分法快速定位到对应的槽,而后再遍历该槽对应分组中的记录便可快速找到指定的记录。也了解到了在页中各个部分的做用是啥,若是没看的,建议回去看一下。数组
附上地址: InnoDB的数据页结构数据结构
首先,咱们先来了解一下若是没有索引的话,当咱们查找一条记录的时候是怎样进行的,固然,咱们就说精准匹配的时候,先附上一句SQL语句:post
SELECT column FROM table WHERE column = xxx;
复制代码
上面这个类型的语句是咱们经常使用的,也比较简单,下面咱们来看一下:ui
假设这个表中的数据量比较小,只有一页的数据,这个时候的查找分为如下状况:spa
Page Directory
,经过二分法快速定位到对应的槽,而后再遍历该槽对应分组中的记录便可快速找到指定的记录。上面的状况是一种假设,但真实的状况仍是须要如今整个居多,一个表中的记录通常都是有不少的数据页组成的,同时,在多个数据页中的查找方式是这样的:设计
咱们先准备一个表:3d
mysql> CREATE TABLE index_demo(
-> c1 INT,
-> c2 INT,
-> c3 CHAR(1),
-> PRIMARY KEY(c1)
-> ) ROW_FORMAT = Compact;
Query OK, 0 rows affected (0.03 sec)
mysql>
复制代码
这个表使用Compact
行格式来实际存储记录的。为了咱们理解上的方便,咱们简化了一下index_demo
表的行格式示意图:
先介绍一下上面几个部分表明的含义:
record_type
: 记录头信息的一项属性,表示记录的类型,0
表示普通记录、2
表示最小记录、3
表示最大记录、1
咱们还没用过,等会再说~next_record
: 记录头信息的一项属性,表示下一条地址的偏移量,为了方便你们理解,咱们都会用箭头来代表下一条记录是谁。数据列
:就是各个数据列的值,其中咱们用橘黄色的格子表明c1
列,深蓝色的格子表明c2
列,红色格子表明c3
列。其它信息
:除了上述 3 种信息之外的全部信息,包括其余隐藏列的值以及记录的额外信息。但放入一些记录以后的在页的图以下:
刚才说了,为何找记录对应的页的时候须要依次遍历查找呢?由于没有对应页的目录,没有的话怎么办呢?建一个不就好了?咱们来看看。
咱们知道一页中的记录是按照大小进行依次连接的单向链表,因此,咱们使用页创建目录也须要遵照一样的规则,因此咱们首先须要保障第二页的记录的主键值是大于第一页的。因此就有了一个前提了。
为了下面咱们更好的说明,咱们先作一个假设:
假设咱们的每一个数据页最多能存放 3 条记录(实际不是),有了这个假设以后咱们向
index_demo
表插入 3 条记录:
mysql> INSERT INTO index_demo VALUES(1, 4, 'u'), (3, 9, 'd'), (5, 3, 'y');
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql>
复制代码
那么页中的图以下:
按主键进行大小排序的单向链表;
上面咱们作了假设,一个页中最多只能放三条记录,这个时候咱们再插入一条数据:
mysql> INSERT INTO index_demo VALUES(4, 4, 'a');
Query OK, 1 row affected (0.00 sec)
mysql>
复制代码
这个时候应该从新再分配一个页:
咦?怎么分配的页号是28
呀,不该该是11
么?须要注意的一点是,新分配的数据页编号可能并非连续的,也就是说咱们使用的这些页在存储空间里可能并不挨着。
上面咱们也说了,要先创建目录须要遵照规则,上面图中的页明显没有,因此须要进行移动,过程以下:
这个过程代表了在对页中的记录进行增删改操做的过程当中,咱们必须经过一些诸如记录移动的操做来始终保证这个状态一直成立:下一个数据页的主键值必须大于上一个页中的主键值。
上面说到条件已经知足了,下面须要进行目录的创建了。
咱们接着往表中插入数据,获得如下的结构:
注:数据页的编号可能并非连续的
如今咱们来针对每个页来创建目录项,每一个目录项包含如下两个部分:
见下图:
看到这样的图了,你们再来想一想,咱们只须要把几个目录项在物理存储器上连续存储,好比把他们放到一个数组里,就能够实现根据主键值快速查找某条记录的功能了。比方说咱们想找主键值为20
的记录,咱们来看一下查找记录的过程:
20
的记录在目录项3
中(由于 12 < 20 < 209
),它对应的页是页9
。页9
中定位具体的记录。针对数据页作的简易目录就搞定了,怎么样?这样是否是好多了,可能你们也知道了,没错,这个目录也被咱们称做 索引
。
上面的方案是一个简单的索引方案,由于咱们假设全部目录项均可以在物理存储器上连续存储,这样的方案存在几个问题:
忠于以上的状况,咱们须要有更好的方式。
设计InnoDB
的大叔们须要一种能够灵活管理全部目录项
的方式。他们灵光乍现,突然发现这些目录项
其实长得跟咱们的用户记录差很少,只不过目录项
中的两个列是主键
和页号
而已,因此他们复用了以前存储用户记录的数据页来存储目录项,为了和用户记录作一下区分,咱们把这些用来表示目录项的记录称为目录项记录
。那InnoDB
怎么区分一条记录是普通的用户记录仍是目录项记录
呢?别忘了记录头信息里的record_type
属性,它的各个取值表明的意思以下:
0
:普通的用户记录1
:目录项记录2
:最小记录3
:最大记录原来这个值为1
的record_type
是这个意思呀,咱们把前边使用到的目录项放到数据页中的样子就是这样:
咱们来讲一下目录项记录
和用户记录
的区别:
目录项记录
的record_type
值是 1,而普通用户记录的record_type
值是 0。目录项记录
只有主键值和页的编号两个列,而普通的用户记录的列是用户本身定义的,可能包含不少列,另外还有InnoDB
本身添加的隐藏列。除了上述几点外,这二者就没啥差异了,它们用的是同样的数据页,页的组成结构也是同样同样的(就是咱们前边介绍过的 7 个部分),都会为主键值生成Page Directory
(页目录)以加快在页内的查询速度。
因此如今根据某个主键值去查找记录的步骤能够大体拆分红下边两步,以查找主键为20
的记录为例(由于都是从一个页中经过主键查某条记录,因此均可以使用Page Directory
经过二分法而实现快速查找):
目录项记录
的页中经过二分法快速定位到对应目录项,由于12 < 20 < 209
,因此定位到对应的记录所在的页就是页9
。页9
中根据二分法快速定位到主键值为20
的用户记录(这个过程再也不多说)。虽说目录项记录中只是存储主键值和对应的页号,因为一个页中只有16KB的大小,能存放的目录项记录也是有限的,因此当一个页存储目录项满了以后再有的话就须要再来一个存储目录项记录的页。
为了你们更好的理解如何新分配一个目录项记录
页的过程,咱们假设一个存储目录项记录
的页最多只能存放 4 条目录项记录
(请注意是假设哦,真实状况下能够存放好多条的),因此若是此时咱们再向上图中插入一条主键值为320
的用户记录的话,那就须要一个分配一个新的存储目录项记录
的页喽:
以上图中,因为咱们新增了一条记录,因此获得了一个新的数据页,里面存放的是数据记录,又由于页30的页目录记录存储满了(上面作了假设,假设每页最多只能存储4条),因此有了页32来存放页31对应的目录项。
由于存储目录项记录
的页不止一个,因此若是咱们想根据主键值查找一条用户记录大体须要 3 个步骤:
目录项记录
页;
目录项记录
的页有两个,即页30
和页32
,又由于页30
表示的目录项的主键值的范围是[1, 320)
,页32
表示的目录项的主键值不小于320
,因此主键值为20
的记录对应的目录项记录在页30
中。目录项记录
页肯定用户记录真实所在的页;
目录项记录
中定位一条目录项记录的方式说过了(经过二分查找进行定位,找到对应的页)。那么问题来了,在这个查询步骤的第 1 步中咱们须要定位存储目录项记录
的页,可是这些页在存储空间中也可能不挨着,若是咱们表中的数据很是多则会产生不少存储目录项记录
的页,那咱们怎么根据主键值快速定位一个存储目录项记录
的页呢?
其实也简单,为这些存储目录项记录
的页再生成一个更高级的目录,就像是一个多级目录同样,大目录里嵌套小目录,小目录里才是实际的数据,因此如今各个页的示意图就是这样子:
如图,咱们生成了一个存储更高级目录项的页33
,这个页中的两条记录分别表明页30
和页32
,若是用户记录的主键值在[1, 320)
之间,则到页30
中查找更详细的目录项记录
,若是主键值不小于320
的话,就到页32
中查找更详细的目录项记录
。
随着表中记录的增长,这个目录的层级会继续增长,若是简化一下,那么咱们能够用下边这个图来描述它:
其实这是一种组织数据的形式,或者说是一种数据结构,它的名称是B+
树。
由于咱们把数据页都存放到B+
树这个数据结构中了,因此咱们也把咱们的数据页称为节点
。从图中能够看出来,咱们的实际用户记录其实都存放在 B + 树的最底层的节点上,这些节点也被称为叶子节点
或叶节点
,其他的节点都是用来存放目录项
的,这些节点通通被称为内节点
或者说非叶节点
。其中最上边的那个节点也称为根节点
。
从图中能够看出来,一个B+
树的节点其实能够分红好多层,设计InnoDB
的大叔们为了讨论方便,规定最下边的那层,也就是存放咱们用户记录的那层为第0
层,以后依次往上加。上边咱们作了一个很是极端的假设,存放用户记录的页最多存放 3 条记录,存放目录项记录的页最多存放 4 条记录,其实真实环境中一个页存放的记录数量是很是大的,假设,假设,假设全部的数据页,包括存储真实用户记录和目录项记录的页,均可以存放1000
条记录,那么:
B+
树只有 1 层,也就是只有 1 个用于存放用户记录的节点,最多能存放1000
条记录。B+
树有 2 层,最多能存放1000×1000=1000000
条记录。B+
树有 3 层,最多能存放1000×1000×1000=1000000000
条记录。B+
树有 4 层,最多能存放1000×1000×1000×1000=1000000000000
条记录。你的表里能存放1000000000000
条记录么?因此通常状况下,咱们用到的B+
树都不会超过 4 层,那咱们经过主键去查找某条记录最多只须要作 4 个页面内的查找,又由于在每一个页面内有所谓的Page Directory
(页目录),因此在页面内也能够经过二分法实现快速定位记录。
上面所说的B+树,咱们知道了B+树自己就是一个目录,或者说它自己就是一个索引,它有如下特色:
B+
树的叶子节点存储的是完整的用户记录。
咱们把具备这两种特性的B+
树称为聚簇索引
,全部完整的用户记录都存放在这个聚簇索引
的叶子节点处;
换句话说主键索引就是聚簇索引;
聚簇索引
并不须要咱们在MySQL
语句中显式的去建立,InnoDB
存储引擎会自动的为咱们建立聚簇索引。另外有趣的一点是,在InnoDB
存储引擎中,聚簇索引
就是数据的存储方式(全部的用户记录都存储在了叶子节点
),也就是所谓的索引即数据。
上面也说到了聚簇索引是针对主键值时才能发挥做用,那么当索引为其它列的时候,又是怎样的呢?难道只能从头至尾沿着链表依次遍历记录么?
不,咱们能够多建几棵B+
树,不一样的B+
树中的数据采用不一样的排序规则。比方说咱们用c2
列的大小做为数据页、页中记录的排序规则,再建一棵B+
树,效果以下图所示:
这个B+树与上边介绍的聚簇索引有几处不一样:
因此若是咱们如今想经过c2
列的值查找某些记录的话就可使用咱们刚刚建好的这个B+
树了,以查找c2
列的值为4
的记录为例,查找过程以下:
目录项记录
页
根页面
,也就是页44
,能够快速定位到目录项记录
所在的页为页42
(由于2 < 4 < 9
)。目录项记录
页肯定用户记录真实所在的页。
页42
中能够快速定位到实际存储用户记录的页,可是因为c2
列并无惟一性约束,因此c2
列值为4
的记录可能分布在多个数据页中,又由于2 < 4 ≤ 4
,因此肯定实际存储用户记录的页在页34
和页35
中。页34
和页35
中定位到具体的记录。B+
树的叶子节点中的记录只存储了c2
和c1
(也就是主键
)两个列,因此咱们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录。你们可能页看到了,当最后定位到对应记录的时候,获得的是一个主键,而获得主键后仍然须要到聚簇索引
中再查一遍,这个过程也被称为回表
。也就是根据c2
列的值查询一条完整的用户记录须要使用到2
棵
B+
树!!!
可能您会想,为何须要回表呢?直接查出来不行吗?
固然能够,可是您想一想,一个表中,每当咱们创建一个索引就须要把记录拷贝一份到B+树,是否是太浪费存储空间了。由于这种按照非主键列
创建的B+
树须要一次回表
操做才能够定位到完整的用户记录,因此这种B+
树也被称为二级索引
或者辅助索引
。
咱们有时候也会使用多个列作联合索引,也就是同时为多个列创建索引,比方说咱们想让B+
树按照c2
和c3
列的大小进行排序,这个包含两层:
c2
列进行排序。c2
列相同的状况下,采用c3
列进行排序为c2
和c3
列创建的索引的示意图以下:
目录项记录
都由c2
、c3
、页号
这三个部分组成,各条记录先按照c2
列的值进行排序,若是记录的c2
列相同,则按照c3
列的值进行排序。B+
树叶子节点处的用户记录由c2
、c3
和主键c1
列组成。以 c2 和 c3 列的大小为排序规则创建的B+
树称为联合索引
,它的意思与分别为 c2 和 c3 列创建索引的表述是不一样的,不一样点以下:
联合索引
只会创建如上图同样的 1 棵B+
树。c2
和c3
列的大小为排序规则创建 2 棵B+
树。InnoDB
存储引擎来讲,在单个页中查找某条记录分为两种状况:
Page Directory
经过二分法快速定位相应的用户记录。InnoDB
存储引擎的索引是一棵B+
树,完整的用户记录都存储在B+
树第0
层的叶子节点,其余层次的节点都属于内节点
,内节点
里存储的是目录项记录
。InnoDB
的索引分为两大种:
列 + 主键
,因此每次查找的数据都会先获得主键,而获得主键后仍然须要到聚簇索引
中再查一遍,这个过程也被称为回表
。也就是根据c2
列的值查询一条完整的用户记录须要使用到2
棵
B+
树!!!最后说一下,本文的参考文章: MySQL的索引 。
本文的不少内容也是来自这篇文章,本人只在文章中插入了有关本身对于文章的理解,若是说的不对,还望指教。
你们也能够去看一下原文。