前言
关于索引,这是一个非常重要的知识点,同样,在面试的时候也会被经常的问到;
本文描述了索引的结构,介绍了InnoDB的索引方案等知识点,感兴趣的可以看一下;
引入
本文参考文章:
回顾
索引
起步
首先,我们先来了解一下如果没有索引的话,当我们查找一条记录的时候是怎样进行的,当然,我们就说精准匹配的时候,先附上一句SQL语句:
SELECT column FROM table WHERE column = xxx;
复制代码
上面这个类型的语句是我们常用的,也比较简单,下面我们来看一下:
在一页中查找
假设这个表中的数据量比较小,只有一页的数据,这个时候的查找分为以下情况:
- 当条件为主键
- 这个过程我们在上篇文章已经说过了,通过页结构中的
Page Directory,通过二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。
- 这个过程我们在上篇文章已经说过了,通过页结构中的
- 当条件是其它列
- 当条件是其它列不是主键的时候,数据页中是没有对应非主键的而建立页目录的,所以无法像主键那样通过二分查找定位,只能通过最笨的方法,直接遍历整个数据页一条一条的进行匹配。当然,这种方法的效率就不说了。
在多页中查找
上面的情况是一种假设,但真实的情况还是需要现在整个居多,一个表中的记录一般都是有很多的数据页组成的,同时,在多个数据页中的查找方式是这样的:
- 首先需要找到该记录对应的页
- 上面假设的是只有一页的数据,所以我们根据对应主键的而建立的页目录进行查找。但是现在没有针对页的页目录,所以我们不能快速的定位到记录所在的页,就只能从第一页开始进行遍历进行慢慢从查找,这样一说可能您看着就头皮发麻了,要是很多记录怎么办?那得等到什么时候?
- 从页中查找对应的记录。
- 这个过程我们就不再说了
准备
我们先准备一个表:
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么?需要注意的一点是,新分配的数据页编号可能并不是连续的,也就是说我们使用的这些页在存储空间里可能并不挨着。
这个过程表明了在对页中的记录进行增删改操作的过程中,我们必须通过一些诸如记录移动的操作来始终保证这个状态一直成立:下一个数据页的主键值必须大于上一个页中的主键值。
- 目录建立
我们接着往表中插入数据,得到以下的结构:
注:数据页的编号可能并不是连续的
- 页目录中的一部分是存储的该页中最小的主键值,我们用key来表示;
- 页目录中的另一部分是页号,我们用page_no表示;
见下图:
- 先从目录项中根据二分法快速确定出主键值为
20的记录在目录项3中(因为12 < 20 < 209),它对应的页是页9。 - 再根据前边说的在页中查找记录的方式去
页9中定位具体的记录。
InnoDB的索引方案
- InnoDB是使用页作为管理存储空间的基本单位,最多只能保证16KB的连续存储空间,当表中的记录慢慢增多的时候就需要非常大的连续的存储空间才能把所有的目录放下。
- 再从我们常做的操作来分析,当我们对表中的记录进行增删的时候,假设我们对以上页28中的记录进行删除,那么页28页就没有存在的必要了,同时目录2也一样没有存在的必要,这个时候就需要把目录2后面的目录项往前移动,影响这么大可不是什么好办法。
忠于以上的情况,我们需要有更好的方式。
0:普通的用户记录1:目录项记录2:最小记录3:最大记录
所以现在根据某个主键值去查找记录的步骤可以大致拆分成下边两步,以查找主键为20的记录为例(因为都是从一个页中通过主键查某条记录,所以都可以使用Page Directory通过二分法而实现快速查找):
- 先到存储
目录项记录的页中通过二分法快速定位到对应目录项,因为12 < 20 < 209,所以定位到对应的记录所在的页就是页9。 - 从
页9中根据二分法快速定位到主键值为20的用户记录(这个过程不再多说)。
- 确定
目录项记录页;- 我们现在的存储
目录项记录的页有两个,即页30和页32,又因为页30表示的目录项的主键值的范围是[1, 320),页32表示的目录项的主键值不小于320,所以主键值为20的记录对应的目录项记录在页30中。
- 我们现在的存储
- 通过
目录项记录页确定用户记录真实所在的页;- 在一个存储
目录项记录中定位一条目录项记录的方式说过了(通过二分查找进行定位,找到对应的页)。
- 在一个存储
- 在真实存储用户记录的页中定位到具体的记录;
- 不多说了。
其实这是一种组织数据的形式,或者说是一种数据结构,它的名称是B+树。
- 如果
B+树只有 1 层,也就是只有 1 个用于存放用户记录的节点,最多能存放1000条记录。 - 如果
B+树有 2 层,最多能存放1000×1000=1000000条记录。 - 如果
B+树有 3 层,最多能存放1000×1000×1000=1000000000条记录。 - 如果
B+树有 4 层,最多能存放1000×1000×1000×1000=1000000000000条记录。
聚簇索引
- 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:
- 页内的记录是按照主键的大小顺序排成一个单向链表;
- 各个存放用户记录的页也是根据页中记录的主键大小顺序排成一个双向链表;
- 各个存放目录项的页也是根据页中记录最小值的主键大小顺序排成一个双向链表;
B+树的叶子节点存储的是完整的用户记录。- 所谓完整的用户记录,就是指这个记录中存储了所有列的值。
我们把具有这两种特性的B+树称为聚簇索引,所有完整的用户记录都存放在这个聚簇索引的叶子节点处;
换句话说主键索引就是聚簇索引;
聚簇索引并不需要我们在MySQL语句中显式的去创建,InnoDB存储引擎会自动的为我们创建聚簇索引。另外有趣的一点是,在InnoDB存储引擎中,聚簇索引就是数据的存储方式(所有的用户记录都存储在了叶子节点),也就是所谓的索引即数据。
二级索引
上面也说到了聚簇索引是针对主键值时才能发挥作用,那么当索引为其它列的时候,又是怎样的呢?难道只能从头到尾沿着链表依次遍历记录么?
不,我们可以多建几棵B+树,不同的B+树中的数据采用不同的排序规则。比方说我们用c2列的大小作为数据页、页中记录的排序规则,再建一棵B+树,效果如下图所示:
这个B+树与上边介绍的聚簇索引有几处不同:
- 使用记录c2列的大小进行记录和页排序
- 页内是按照c2列的大小进行排序形成的一个单向链表。
- 各个存放用户记录的页也是根据页中记录的c2列大小顺序排成的一个双向链表。
- 各个存放目录项的页根据页中记录的c2列的大小排成的双向链表。
- B+树的叶子节点并不是完整的用户记录,而是c2列+主键这两个列的值
- 目录项记录不再是主键+页号,而是c2列+页号
所以如果我们现在想通过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 列建立索引会分别以
c2和c3列的大小为排序规则建立 2 棵B+树。
总结
- 对于
InnoDB存储引擎来说,在单个页中查找某条记录分为两种情况:- 以主键为搜索条件,可以使用
Page Directory通过二分法快速定位相应的用户记录。 - 以其他列为搜索条件,需要按照记录组成的单链表依次遍历各条记录。
- 以主键为搜索条件,可以使用
- 没有索引的情况下,不论是以主键还是其他列作为搜索条件,只能沿着页的双链表从左到右依次遍历各个页。
InnoDB存储引擎的索引是一棵B+树,完整的用户记录都存储在B+树第0层的叶子节点,其他层次的节点都属于内节点,内节点里存储的是目录项记录。InnoDB的索引分为两大种:- 聚簇索引
- 以主键值的大小为页和记录的排序规则,在叶子节点处存储的记录包含了表中所有的列(索引既数据)。
- 二级索引
- 以自定义的列的大小为页和记录的排序规则,在叶子节点处存储的记录内容是
列 + 主键,所以每次查找的数据都会先得到主键,而得到主键后仍然需要到聚簇索引中再查一遍,这个过程也被称为回表。也就是根据c2列的值查询一条完整的用户记录需要使用到2棵B+树!!!
- 以自定义的列的大小为页和记录的排序规则,在叶子节点处存储的记录内容是
- 聚簇索引
最后
最后说一下,本文的参考文章: 。
本文的很多内容也是来自这篇文章,本人只在文章中插入了有关自己对于文章的理解,如果说的不对,还望指教。
大家也可以去看一下原文。