我们在学习MySQL的时候经常会听到索引这个词,大概也知道这是什么,但是深究下去又说不出什么道道来。下面将会比较全面的介绍一下关于索引!
1 索引是什么?
这里用百度百科的一句话来说,在关系数据库中,索引是一种单独的、物理的对数据库表中一列或多列的值进行排序的一种存储结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。
简单来说,索引就是我们一本书的目录,通过目录我们才能更快在一本书中查找到我们所要看的内容。同样的,通过索引我们才能在数据库中查找到我们的数据!
2 没使用索引的MySQL
我们知道索引可以加快我们的查找,所以这里通过没有使用索引的查找可以更加地让我们认识到使用索引的好处。
我们的MySQL基本的页存储结构是页,也就是我们的数据记录都在页里面。
当我们插入一条记录的时候就会存储在我们的数据页中的存放行记录的位置,并在我们的Page Directory页目录那里生成主键的信息。我们的数据页中记录又可以组成一个单链表,每插入一条数据就会在尾节点那里添加上。
当我们通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录。如果不是主键的话,那么只能遍历单链表中的每条记录对比查找。
所以,如果不用索引优化的话,那么在进行一条查找的sql的话,默认的流程是这样子的:
- 定位到记录所在的页(需要遍历双向链表,找到所在的页)
- 从所在的页内中查找相应的记录(是不是根据主键查询,不是只能遍历所在页的单链表了)
如果在数据量特别大的时候,又是极端情况,遍历双向链表和单链表,速度就会显得非常慢!
3 B-Tree索引与B+Tree索引
3.1 B-Tree索引结构
当人们开始谈论索引的时候,如果没有特别指明类型的话,那么多半说的就是B-Tree(B树)所以,它使用的是B-Tree数据结构来存储数据,大多数的MySQL引擎都支持这种索引(但实际上很多存储引擎使用的是B+Tree,这个我们稍后再谈到)。我们这里通过B-Tree索引结构就可以极大的优化了上面的查找。
从图中我们可以很明显的感受到,使用索引后,就不需要再遍历双向链表那样去查找页,而是通过目录就可以很快的定位到我们的实际的页。如查找id为1的数据
- 首先小于4,可以确定在p1下,指向磁盘页2
- 在磁盘页里面,小于2,指向p1,然后查找到
而且,我们也可以根据图总结出B-Tree的特点:
- 所有键值数据分布在整棵树各个节点中
- 我们的查找有可能在非节点结束,比如上图中,我们要找id为4的数据的话,在根节点就可以查找到
- 所有叶子节点都在同一层,并且以升序排列
3.2 B+Tree索引结构
B+Tree索引是依据B-Tree索引基础上的一次优化,具体变化如下:
B+Tree 非叶子节点不存放数据
叶子节点存储关键字和数据,非叶子节点的关键字也会沉到叶子节点,并且排序
叶子节点两两指针相互连接,形成一个双向环形链表(符合磁盘的预读特性),顺序查询性能更高(区间查找更加方便)
我们的B+Tree的优化到底有什么好处呢?
首先是我们的数据只放在了叶子节点上面。这个唯一的好处就是我们的非叶子节点可以存放更多的关键字了,整体就可以存放更多的数据。因为我们MySQL查询过程是按页加载数据的,每加载一页就是一次IO操作,我们根磁盘页存放的数据越少,关键字越多,那么整体的数据量就可以说是越多。
还有一个好处就是,在叶子节点形成双向环形链表。这样子如果要进行区间查询的话,只需要顺着叶子节点的指针向下查询就行。而如果是B-Tree的话,就需要返回上一级节点然后再读取磁盘页进行查找,节省了不少时间!
3.3 为什么不采用别的树结构?
为什么要采用B-Tree的结构,甚至是B+Tree的结构呢?
其实还是跟我们的磁盘读取的原因有关。上面我们说到了,MySQL查询过程是按页加载数据的。而我们的操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。我们的内存每次读取的就是MySQL分割成的一个页大小,也就是一个索引点,图中的一个磁盘页。
采用普通二叉树?不!
如果采用普通的二叉树的话,我们要考虑到一种情况。那就是在极端的情况下,一棵树是会退化成链表的,那么树的优点就没有了。 这与我们原来用双向链表有何异同?
采用红黑树?不!
那么可能有人说了,如果采用红黑树,树保持平衡不就行了吗?确实,红黑树等平衡树也可以用来实现索引,但是与我们的B-Tree/B+Tree来说性能要差很多。
我们上面说到了内存每一次I/O都是载入一个索引节点,也就是一个磁盘页。如果数据不在同一个磁盘块上,那么通常需要移动制动手臂进行寻道,而制动手臂因为其物理结构导致了移动效率低下,从而增加磁盘数据读取时间。B-Tree/B+Tree相对于红黑树有更低的树高,进行寻道的次数与树高成正比,在同一个磁盘块上进行访问只需要很短的磁盘旋转时间,所以 B-Tree/B+Tree 树更适合磁盘数据的读取。
而且为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的磁盘旋转时间,速度会非常快。并且可以利用预读特性,相邻的节点也能够被预先载入。
4 哈希索引
MySQL除了B+树之外,还有一种常见的是就是哈希索引。
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
本质上就是把键值换算成新的哈希值,根据这个哈希值来定位。
使用哈希索引最大的好处就是速度特别快,我们只需要一次定位就可以找到我们要的数据。时间复杂度为O(1),但是我们的InnoDB(MySQL默认存储引擎)默认使用的却是B+树索引,这也是因为哈希索引有一定的缺点:
- 无法用于排序和分组
- 只支持精确查找,无法用于部分查找和范围查找
- 在有大量重复键值情况下,哈希索引的效率也是极低的---->哈希碰撞问题。
- 不支持最左匹配原则
可是如果一个索引值被频繁使用的话,我们的InnoDB会再B+Tree索引之上再创建一个哈希索引,用来方便快速查找。这个功能叫做“自适应哈希索引”。
5 聚簇索引与辅助索引
MySQL数据库中innodb存储引擎,B+树索引可以分为聚簇索引(也称聚集索引,clustered index)和辅助索引(有时也称非聚簇索引或二级索引,secondary index,non-clustered index)。这两种索引内部都是B+树,聚集索引的叶子节点存放着一整行的数据。
Innodb中的主键索引是一种聚簇索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引。
5.1 聚簇索引
聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。
Innodb通过主键聚集数据,如果没有定义主键,innodb会选择非空的唯一索引代替。如果没有这样的索引,innodb会隐式的定义一个主键来作为聚簇索引。
使用聚簇索引的优点:
- 数据访问更快,因为聚簇索引将索引和数据保存在同一个B+树中,因此从聚簇索引中获取数据比非聚簇索引更快
- 聚簇索引对于主键的排序查找和范围查找速度非常快
缺点:
- 插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于InnoDB表,我们一般都会定义一个自增的ID列为主键
- 更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB表,我们一般定义主键为不可更新。
- 二级索引(辅助)访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。
Innodb中聚簇索引示意图:
InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形。
5.2 辅助索引
在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory(页目录)找到数据行。
Innodb辅助索引的叶子节点并不包含行记录的全部数据,叶子节点除了包含键值外,还包含了相应行数据的聚簇索引键。辅助索引的存在不影响数据在聚簇索引中的组织,所以一张表可以有多个辅助索引。在innodb中有时也称辅助索引为二级索引。
Innodb中辅助索引示意图:
通过对比我们就可以知道为什么我们对主键会有要求:
1、为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。
2、为什么用非单调的字段作为主键在InnoDB中不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。
当然,如果我们通过索引优化,将辅助索引优化成覆盖索引,那么辅助索引也包含所有需要查询的字段的值。也就是我们的索引就是我们要的值,无需再访问主索引了。这里,涉及到索引的优化不过多介绍!
5.3 MyISAM实现对比
上面我介绍了在InnoDB存储引擎下的聚簇索引与辅助索引的实现,因为如果没有说明具体的数据库和存储引擎,默认指的是MySQL中的InnoDB存储引擎。但是我们MySQL还支持MyISAM存储引擎,它也是支持聚簇索引与辅助索引的。
但是该引擎下的实现却有些不同,我们的聚簇索引和辅助索引没有什么区别,他们的叶子节点都不存放数据,而是存放数据记录的地址。唯一的区别就是聚簇索引要求key是唯一的,而辅助索引的key可以重复。
所以如果严格的按照聚簇索引叶子节点存放数据来定义的话,MyISAM的索引都只能算是非聚簇索引!聚簇索引,或者严格说主键索引示意图:
辅助索引示意图:
为了更形象说明这两种存储引擎下两种索引的区别,我们假想一个表如下图存储了4行数据。其中Id作为主索引,Name作为辅助索引。图示清晰的显示了聚簇索引和非聚簇索引的差异。
5.4 索引优点及使用
我们通过结构对比了使用索引的好处,总结下来的话就是:
- 大大减少了服务器需要扫描的数据行数
- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)
- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)
但是我们要知道,索引并不是最好的解决方案。总的来说,只有当索引帮助存储引擎快速找到记录带来的好处大于其带来的额外工作时,索引才是有效的。
- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
- 对于中到大型的表,索引就非常有效;
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术(具体可以查看高性能MySQL第七章)。
6 总结
这里仅仅是介绍了索引结构原理等,关于索引还有很多,如全文索引,空间索引等,以及索引的优化之类,这更多是我们要去学习的。
7 参考资料
高性能MySQL(第三版)