一、B+树索引
索引的目的在于提高查询效率,可以类比字典,如果要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql。如果没有索引,那么你可能需要把所有单词看一遍才能找到你想要的,如果我想找到m开头的单词呢?或者ze开头的单词呢?是不是觉得如果没有索引,这个事情根本无法完成?
我们都知道CPU是很快的,磁盘是很慢的,要想提高数据库的访问效率,可以说非常大的一个优化点就是减少磁盘IO访问。每次查找数据时把磁盘IO次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,B+树应运而生。B+树索引的本质就是B+树在数据库中的实现,但是B+树索引在数据库中有一个特点是高扇出性,因此在数据库中,B+树的高度一般都在2-4层,这也就是说查找某一键值得行记录最多只需要2-4次IO。这倒不错,因为当前一般的机械磁盘每秒至少可以做100次IO,2-4次的IO意味着查询时间只需要0.02-0.04秒。
可以说数据库必须有索引,没有索引则检索过程变成了顺序查找,O(n)的时间复杂度几乎是不能忍受的。我们非常容易想象出一个只有单关键字组成的表如何使用B+树进行索引,只要将关键字存储到树的节点即可。当数据库一条记录里包含多个字段时,一棵B+树就只能存储主键,如果检索的是非主键字段,则主键索引失去作用,又变成顺序查找了。这时应该在第二个要检索的列上建立第二套索引。这个索引由独立的B+树来组织。有两种常见的方法可以解决多个B+树访问同一套表数据的问题,一种叫做聚簇索引(clustered index ),一种叫做非聚簇索引(secondary index)。这两个名字虽然都叫做索引,但这并不是一种单独的索引类型,而是一种数据存储方式。对于聚簇索引存储来说,行数据和主键B+树存储在一起,辅助键B+树只存储辅助键和主键,主键和非主键B+树几乎是两种类型的树。对于非聚簇索引存储来说,主键B+树在叶子节点存储指向真正数据行的指针,而非主键。
二、聚簇索引
InnoDB存储引擎表是索引组织表,即表中数据按照主键顺序存放。而聚簇索引就是按照每张表的主键构造一棵B+树,同时叶子节点中存放的即为整张表的行记录数据,也将聚集索引的叶子节点称为数据页。聚簇索引的这个特性决定了索引组织表中数据也是索引的一部分。同B+树数据结构一样,每个数据页都通过一个双向链表来进行链接。由于实际的数据页只能按照一棵B+树进行排序,因此每张表只能拥有一个聚集索引。在多数情况下,查询优化器倾向于采用聚集索引。因为聚集索引能够在B+树索引的叶子节点上直接找到数据。此外,由于定义了数据的逻辑顺序,聚集索引能够特别快地访问针对范围值的查询。查询优化器能够快速发现某一段范围的数据页需要扫描。
许多文档会告诉我们:聚集索引按照顺序物理地存储数据,但是试想一下,如果聚集索引必须按照特定顺序存放物理记录,则维护成本显得非常之高。所以,聚集索引的存储并不是物理上连续的,而是逻辑上连续的。这其中有两点:一是前面说过的页通过双向链表链接,页按照主键的顺序排序;另一点是每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。
聚集索引的另一个好处是,它对于主键的排序查找和范围查找速度非常快。叶子节点的数据就是用户所要查询的数据。如用户需要查询一张注册用户的表,查询最后注册的10位用户,由于B+树索引是双向链表的,用户可以快速找到最后一个数据页。并取出10条记录。若用命令EXPLAIN进行分析,可得:
1
2
3
4
5
6
7
|
mysql> explain select * from sbtest1 order by id limit 100;
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------+
| 1 | SIMPLE | sbtest1 | index | NULL | PRIMARY | 4 | NULL | 100 | |
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------+
1 row in set (0.00 sec)
|
可以看到虽然使用ORDER BY对记录进行排序,但是在实际过程中并没有进行所谓的filesort操作,而这就是因为聚集索引的特点。
另一个是范围查询,即如果要查找主键某一范围内的数据,通过叶子节点的上层中间节点就可以得到页的范围,之后直接读取数据页即可。又如:
1
2
3
4
5
6
7
|
mysql> explain select * from sbtest1 where id>10 and id<1000;
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | sbtest1 | range | PRIMARY | PRIMARY | 4 | NULL | 1946 | Using where |
+------+-------------+---------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)
|
执行EXPLAIN得到了MySQL数据库的执行计划,并且在rows列中给出了一个查询结果的预估返回行数。要注意的是,rows代表的是一个预估值,不是确切的值。
如下图,是一颗B+树(聚簇索引):
关于b+树的定义可以参见B+树,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3。P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。
# b+树性质
1. 通过上面的分析,我们知道IO次数取决于b+数的高度h。假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小。而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。
2. 当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的搜索方向,如果name相同再依次比较age和sex,最后得到检索的数据。但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了, 这个是非常重要的性质,即索引的最左匹配特性。
我们在查询数据时,一般都会在经常被查询的字段上面建立一个索引(B+树索引),这正是利用了索引中被排序的键值,通过内节点的索引功能及叶子节点中数据的有序性(索引默认是升序的),利用二分查找极大的提高了查找的性能,所以索引在数据库中的作用是至关重要的。
# b+树的查找过程
如图所示,如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。
三、辅助索引
对于辅助索引(secondary index,也称二级索引),叶子节点并不包含行记录的全部数据,叶子节点除了包含键值以外,每个叶子节点中的索引行中还包含了一个书签。该书签用来告诉Innodb存储引擎哪里可以找到与索引相对应的行数据。由于Innodb存储引擎是索引组织表,因此Innodb存储引擎的辅助索引的书签就是相应行数据的聚集索引键。
辅助索引的存在并不影响数据在聚集索引中的组织,因此每张表上可以有多个辅助索引。当通过辅助索引来寻找数据时,Innodb存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键,然后再通过主键索引来找到一个完整的行记录。举例来说,如果在一棵高度为3的辅助索引树中查找数据,那需要对这棵辅助索引树遍历3次找到指定主键,如果聚集索引树的高度同样为3,那么还需要对聚集索引树进行3次查找,最终找到一个完整的行数据所在的页。因此一共需要6次逻辑IO访问以得到最终的一个数据页。
从上面的特性我们可以知道,一个表中,聚簇索引占用的空间肯定是最大的,因为它是存储了全部数据的,而辅助索引,是建立在某几个需要经常查询的列上面的,除了这几个列之外,剩下的就是用来“回表”的指针信息了,所以相对而言,辅助索引的占用空间都会比聚簇索引小很多,特别是在一个表的列数很多或是这些列中包含大字段的情况下,因为我们一般都不会在大字段上直接建立索引。那这样比较下来,在我们统计一个表总的精确行数时(查COUNT*),一些优化器就会选择表中最小的索引来作为统计的目标索引,因为它占用空间最小,IO也会最小,性能相应的更快一些。
上面说到了“回表”,所谓回表,就是在使用辅助索引时,因为辅助索引只存储了部分数据,如果根据键值查找到的数据不能包括全部目标数据时(就是无法使用到覆盖索引),就需要通过二级索引的指针,也就是键值对中的值,来找到聚簇索引的全部数据,然后根据完整的数据取出所需要的列的过程就称之为回表。这种在二级索引中不能找到所有需要的数据列的现象,被称为非覆盖索引,反之称为覆盖索引。因为回表本身是需要去另一个索引(聚簇索引)中查找数据的,性能必然会受到影响,那为了尽可能的提高性能就需要尽量的减少回表次数,所以可以试着将出现频率非常高的语句中所有使用到的列以合适的顺序建一个二级索引,这样所有需要的列都被这个二级索引覆盖了,就不需要回表了,从而一定程度上提高了性能。这虽然是一个好的做法,但需要去权衡,因为需要考虑语句中涉及到的列数,这个语句出现的频率及最终这个索引的大小。最坏的情况是建一个和聚簇索引差不多大的二级索引,这样一方面是占用空间比较大,另一方面是维护这个二级索引对这个表的整体修改性能也是有影响的,所以各方面都需要去权衡,然后再决定是不是要这样做。
上面还说到了,在统计总行数的时候,可以直接使用二级索引来做,是因为有一个很明显但很重要的前提:每个二级索引与聚簇索引的总行数是一样的,并且一对一。只不过在每一个索引中,数据行的排序顺序不同,可以想象二级索引行与聚簇索引行行之间都有虚线相连,并且二级索引中每一行都有且只有一条虚线指向聚簇索引中的一行数据,而聚簇索引的每一行,都会有相同个数的虚线指进来,这个数目就是二级索引的个数。至于二级索引与聚簇索引究竟是如何连起来的,我们后面会详细讲述。
四、辅助索引的指针
现在已经知道,聚簇索引存储了所有数据,二级索引只存储了部分数据,但二级索引是为了提高性能的,所以经常会被使用到,那如果二级索引中的数据不能满足需求怎么办?这就用到了我们上面提到的“回表”,也就是二级索引中每行记录中指针的作用。
关于聚簇索引及二级索引列之间的逻辑关系,我们分类如下:
- 自定义主键的聚簇索引
索引结构:[主键列][TRXID][ROLLPTR][其它建表创建的非主键列]
参与记录比较的列:主键列
内结点KEY列:[主键列]+PageNo指针
- 未定义主键的聚簇索引
索引结构:[ROWID][TRXID][ROLLPTR][其它建表创建的非主键列]
参与记录比较的列:只ROWID一列而已
内结点KEY列:[ROWID]+PageNo指针
- 自定义主键的二级唯一索引
索引结构:[唯一索引列][主键列]
参与记录比较的列:[唯一索引列][主键列]
内结点KEY列:[唯一索引列]+PageNo指针
- 自定义主键的二级非唯一索引
索引结构:[非唯一索引列][主键列]
参与记录比较的列:[非唯一索引列][主键列]
内结点KEY列:[非唯一索引列][主键列]+PageNo指针
- 未定义主键的二级唯一索引
索引结构:[唯一索引列][ROWID]
参与记录比较的列:[唯一索引列][ROWID]
内结点KEY列:[唯一索引列]+PageNo指针
- 未定义主键的二级非唯一索引
索引结构:[非唯一索引列][ROWID]
参与记录比较的列:[非唯一索引列][ROWID]
内结点KEY列:[非唯一索引列][ROWID]+PageNo指针
通过这六种情况,讲清楚了聚簇索引记录包含的列,二级索引记录包括的列,以及在非叶子节点中分别包含的列,因为索引是用来检索数据的,所以还讲述了用来检查记录时,在二级索引及聚簇索引中,参与比较记录大小的列分别是什么,唯一索引与非唯一索引的区别等。
需要注意的一点是,上面讲述的索引列的顺序关系,与实际索引中记录的物理存储不是一回事,记录的存储格式是记录的格式,而这个是索引在内存中是元组的组织关系,这个元组的顺序体现的就是每个索引自己的逻辑顺序,以什么列建的索引,什么列就会在最前面起到优先排序的作用。
我们这里特别关注一下二级唯一索引的元组逻辑顺序,二级唯一索引中,作为索引本身的索引列,就是我们上面所说的“键”,当这个元组需要回表时,在元组中存储的聚簇索引列信息,就是我们所说的“值”,这样就形成了键值对。而对于二级非唯一索引而言,因为只有索引列本身再加上主键列才能保证索引记录是唯一的,所以这二者合起来才能构成我们所说的“键”,而“值”就为空了,也就是说,二级非唯一索引中,在记录构成方面,非叶结节点只是比叶子节点多了一个PageNo指针信息。
从上面可以看到,二级索引元组中,首先存储的就是每个索引定义的索引列,接着就是这条记录对应的聚簇索引的主键列的值,而主键列是唯一的,所以二级索引回表时对应的记录也是唯一的,这样就形成了一种指针的效果。
不过有一点需要注意一下,二级索引回表时对应的聚簇索引,如果是用户自定义的,有可能是自增列,也有可能是有逻辑意义的单列或者组合列的聚簇索引,如果用户没有自定义,则InnoDB会自动给聚簇索引分配一个主键列,不过是隐藏的列,即我们所熟知的Rowid列。基于此,如果是用户自定义的聚簇索引,则二级索引指针指向的就是聚簇索引所包含的列,如果没有自定义主键,那该指针就指向Rowid列了。另外我们还可以看到聚簇索引(不管是自定义还是默认的ROWID)都会包含TRXID和ROLLPTR两个默认隐藏列,一个是事务ID,一个是回滚指针,这也就是MVCC和事务实现的关键点。
<参考>
InnoDB存储引擎
MySQL运维内参