说说MySQL索引相关

前言

关于索引,这是一个很是重要的知识点,一样,在面试的时候也会被常常的问到;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么?须要注意的一点是,新分配的数据页编号可能并非连续的,也就是说咱们使用的这些页在存储空间里可能并不挨着。

上面咱们也说了,要先创建目录须要遵照规则,上面图中的页明显没有,因此须要进行移动,过程以下:

这个过程代表了在对页中的记录进行增删改操做的过程当中,咱们必须经过一些诸如记录移动的操做来始终保证这个状态一直成立:下一个数据页的主键值必须大于上一个页中的主键值

  • 目录创建

上面说到条件已经知足了,下面须要进行目录的创建了。

咱们接着往表中插入数据,获得如下的结构:

注:数据页的编号可能并非连续的

如今咱们来针对每个页来创建目录项,每一个目录项包含如下两个部分:

  • 页目录中的一部分是存储的该页中最小的主键值,咱们用key来表示;
  • 页目录中的另外一部分是页号,咱们用page_no表示;

见下图:

看到这样的图了,你们再来想一想,咱们只须要把几个目录项在物理存储器上连续存储,好比把他们放到一个数组里,就能够实现根据主键值快速查找某条记录的功能了。比方说咱们想找主键值为20的记录,咱们来看一下查找记录的过程:

  • 先从目录项中根据二分法快速肯定出主键值为20的记录在目录项3中(由于 12 < 20 < 209),它对应的页是页9
  • 再根据前边说的在页中查找记录的方式去页9中定位具体的记录。

针对数据页作的简易目录就搞定了,怎么样?这样是否是好多了,可能你们也知道了,没错,这个目录也被咱们称做 索引

InnoDB的索引方案

上面的方案是一个简单的索引方案,由于咱们假设全部目录项均可以在物理存储器上连续存储,这样的方案存在几个问题:

  • InnoDB是使用页做为管理存储空间的基本单位,最多只能保证16KB的连续存储空间,当表中的记录慢慢增多的时候就须要很是大的连续的存储空间才能把全部的目录放下。
  • 再从咱们常作的操做来分析,当咱们对表中的记录进行增删的时候,假设咱们对以上页28中的记录进行删除,那么页28页就没有存在的必要了,同时目录2也同样没有存在的必要,这个时候就须要把目录2后面的目录项往前移动,影响这么大可不是什么好办法。

忠于以上的状况,咱们须要有更好的方式。

设计InnoDB的大叔们须要一种能够灵活管理全部目录项的方式。他们灵光乍现,突然发现这些目录项其实长得跟咱们的用户记录差很少,只不过目录项中的两个列是主键页号而已,因此他们复用了以前存储用户记录的数据页来存储目录项,为了和用户记录作一下区分,咱们把这些用来表示目录项的记录称为目录项记录。那InnoDB怎么区分一条记录是普通的用户记录仍是目录项记录呢?别忘了记录头信息里的record_type属性,它的各个取值表明的意思以下:

  • 0:普通的用户记录
  • 1:目录项记录
  • 2:最小记录
  • 3:最大记录

原来这个值为1record_type是这个意思呀,咱们把前边使用到的目录项放到数据页中的样子就是这样:

咱们来讲一下目录项记录用户记录的区别:

  1. 目录项记录record_type值是 1,而普通用户记录的record_type值是 0。
  2. 目录项记录只有主键值和页的编号两个列,而普通的用户记录的列是用户本身定义的,可能包含不少列,另外还有InnoDB本身添加的隐藏列。

除了上述几点外,这二者就没啥差异了,它们用的是同样的数据页,页的组成结构也是同样同样的(就是咱们前边介绍过的 7 个部分),都会为主键值生成Page Directory(页目录)以加快在页内的查询速度。

因此如今根据某个主键值去查找记录的步骤能够大体拆分红下边两步,以查找主键为20的记录为例(由于都是从一个页中经过主键查某条记录,因此均可以使用Page Directory经过二分法而实现快速查找):

  • 先到存储目录项记录的页中经过二分法快速定位到对应目录项,由于12 < 20 < 209,因此定位到对应的记录所在的页就是页9
  • 页9中根据二分法快速定位到主键值为20的用户记录(这个过程再也不多说)。

虽说目录项记录中只是存储主键值和对应的页号,因为一个页中只有16KB的大小,能存放的目录项记录也是有限的,因此当一个页存储目录项满了以后再有的话就须要再来一个存储目录项记录的页。

为了你们更好的理解如何新分配一个目录项记录页的过程,咱们假设一个存储目录项记录的页最多只能存放 4 条目录项记录(请注意是假设哦,真实状况下能够存放好多条的),因此若是此时咱们再向上图中插入一条主键值为320的用户记录的话,那就须要一个分配一个新的存储目录项记录的页喽:

以上图中,因为咱们新增了一条记录,因此获得了一个新的数据页,里面存放的是数据记录,又由于页30的页目录记录存储满了(上面作了假设,假设每页最多只能存储4条),因此有了页32来存放页31对应的目录项。

由于存储目录项记录的页不止一个,因此若是咱们想根据主键值查找一条用户记录大体须要 3 个步骤:

  1. 肯定目录项记录页;
    1. 咱们如今的存储目录项记录的页有两个,即页30页32,又由于页30表示的目录项的主键值的范围是[1, 320)页32表示的目录项的主键值不小于320,因此主键值为20的记录对应的目录项记录在页30中。
  2. 经过目录项记录页肯定用户记录真实所在的页;
    1. 在一个存储目录项记录中定位一条目录项记录的方式说过了(经过二分查找进行定位,找到对应的页)。
  3. 在真实存储用户记录的页中定位到具体的记录;
    1. 很少说了。

那么问题来了,在这个查询步骤的第 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列的大小进行记录和页排序
    • 页内是按照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+树的叶子节点中的记录只存储了c2c1(也就是主键)两个列,因此咱们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录。

你们可能页看到了,当最后定位到对应记录的时候,获得的是一个主键,而获得主键后仍然须要到聚簇索引中再查一遍,这个过程也被称为回表 。也就是根据c2列的值查询一条完整的用户记录须要使用到2 B+ 树!!!

可能您会想,为何须要回表呢?直接查出来不行吗?

固然能够,可是您想一想,一个表中,每当咱们创建一个索引就须要把记录拷贝一份到B+树,是否是太浪费存储空间了。由于这种按照非主键列创建的B+树须要一次回表操做才能够定位到完整的用户记录,因此这种B+树也被称为二级索引 或者辅助索引

联合索引

咱们有时候也会使用多个列作联合索引,也就是同时为多个列创建索引,比方说咱们想让B+树按照c2c3列的大小进行排序,这个包含两层:

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

c2c3列创建的索引的示意图以下:

  • 每条目录项记录都由c2c3页号这三个部分组成,各条记录先按照c2列的值进行排序,若是记录的c2列相同,则按照c3列的值进行排序。
  • B+树叶子节点处的用户记录由c2c3和主键c1列组成。

以 c2 和 c3 列的大小为排序规则创建的B+树称为联合索引,它的意思与分别为 c2 和 c3 列创建索引的表述是不一样的,不一样点以下:

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

总结

  • 对于InnoDB存储引擎来讲,在单个页中查找某条记录分为两种状况:
    1. 以主键为搜索条件,可使用Page Directory经过二分法快速定位相应的用户记录。
    2. 以其余列为搜索条件,须要按照记录组成的单链表依次遍历各条记录。
  • 没有索引的状况下,不管是以主键仍是其余列做为搜索条件,只能沿着页的双链表从左到右依次遍历各个页。
  • InnoDB存储引擎的索引是一棵B+树,完整的用户记录都存储在B+树第0层的叶子节点,其余层次的节点都属于内节点内节点里存储的是目录项记录InnoDB的索引分为两大种:
    • 聚簇索引
      • 以主键值的大小为页和记录的排序规则,在叶子节点处存储的记录包含了表中全部的列(索引既数据)。
    • 二级索引
      • 以自定义的列的大小为页和记录的排序规则,在叶子节点处存储的记录内容是列 + 主键 ,因此每次查找的数据都会先获得主键,而获得主键后仍然须要到聚簇索引中再查一遍,这个过程也被称为回表 。也就是根据c2列的值查询一条完整的用户记录须要使用到2 B+ 树!!!

最后

最后说一下,本文的参考文章: MySQL的索引

本文的不少内容也是来自这篇文章,本人只在文章中插入了有关本身对于文章的理解,若是说的不对,还望指教。

你们也能够去看一下原文。

相关文章
相关标签/搜索