数据库索引总结

索引简介

索引是一种数据结构,索引的出现是为了提高数据查询的效率。查询效率一般可以从等值查询和范围查询两个方面进行评判。

索引的模型

哈希表

哈希表是一种以键 - 值(key-value)存储数据的结构,哈希的思路很简单,把值放在数组里,用一个哈希函数把 key 换算成一个确定的位置,然后把 value 放在数组的这个位置。

不同的key 值经过哈希函数的换算,会出现同一个值的情况。处理这种情况的一种方法是,拉出一个链表。

假设,现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,这时对应的哈希索引的示意图如下所示:
数据库索引总结_第1张图片

哈希表由于可以快速的找到数据的位置,等值查询效率非常高,但哈希表中的值因为不保证顺序,如果要查询某个范围内的数据,就需要把整个哈希表中的值进行扫描判断。所以哈希表这种数据结构适合比较适合等值查询,不适合范围查询的场景。

有序数组

有序数组保证了数组中的元素的值是按照一定顺序存储的,还是上面的例子,假设身份证是不重复的,如下图:
数据库索引总结_第2张图片
如果要查 ID_card_n2 对应的名字,用二分法就可以快速得到,这个时间复杂度是 O(log(N))。

同时,这个索引结构支持范围查询。要查身份证号[ID_card_X, ID_card_Y]区间的 User,可以先用二分法找到 ID_card_X(如果不存在 ID_card_X,就找到大于 ID_card_X 的第一个 User),然后向右遍历,直到查到第一个大于 ID_card_Y 的身份证号,退出循环。

如果仅仅看查询效率,有序数组就是比较好的数据结构了。但是,在需要更新数据的时候就比较麻烦了,往中间插入一个记录就必须得挪动后面所有的记录,成本太高。所以,有序数组索引只适用于静态存储引擎。

二叉树

还是上面根据身份证号查名字的例子,如果我们用二叉搜索树来实现的话,示意图如下所示:
数据库索引总结_第3张图片
二叉搜索树的特点是:父节点左子树所有结点的值小于父节点的值,右子树所有结点的值大于父节点的值。这样如果要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2 这个路径得到。这个时间复杂度是 O(log(N))。

当然为了维持 O(log(N)) 的查询复杂度,你就需要保持这棵树是平衡二叉树。为了做这个保证,更新的时间复杂度也是 O(log(N))。

树可以有二叉,也可以有多叉。多叉树就是每个节点有多个儿子,儿子之间的大小保证从左到右递增,如图:
数据库索引总结_第4张图片

二叉树是搜索效率最高的,但是实际上大多数的数据库存储却并不使用二叉树。其原因是,索引不止存在内存中,还要写到磁盘上。

如果数据库存储使用二叉树,那么树的高度就比较大,想象一下一棵 100 万节点的平衡二叉树,树高 20。一次查询可能需要访问 20 个数据块。在机械硬盘时代,从磁盘随机读一个数据块需要 10 ms 左右的寻址时间。也就是说,对于一个 100 万行的表,如果使用二叉树来存储,单独访问一个行可能需要 20 个 10 ms 的时间。

为了让一个查询尽量少地读磁盘,就必须让查询过程访问尽量少的数据块。那么,就不应该使用二叉树,而是要使用“N 叉”树来实现减少树的高度。这里,“N 叉”树中的“N”取决于数据块的大小。

对于相同个数的数据构建 N 叉树索引,N 叉树中的 N 越大,那树的高度就越小,那 N 叉树中的 N 是不是越大越好呢?到底多大才最合适呢?

不管是内存中的数据,还是磁盘中的数据,操作系统都是按页(一页大小通常是 4KB,这个值可以通过 getconfig PAGE_SIZE 命令查看)来读取的,一次会读一页的数据。如果要读取的数据量超过一页的大小,就会触发多次 IO 操作。所以,我们在选择 N 大小的时候,要尽量让每个节点的大小等于一个页的大小。读取一个节点,只需要一次磁盘 IO 操作。

N 叉树由于在读写上的性能优点,以及适配磁盘的访问模式,已经被广泛应用在数据库引擎中了。

InnoDB 的索引

索引模型

InnoDB 使用了 B+ 树索引模型,数据都是存储在 B+ 树中。每一个索引在 InnoDB 里面对应一棵 B+ 树。在B+ 树中,叶子节点通过一个有序的双向链表串起来。

假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在 k 上有索引,表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵 B+ 树的示例示意图如下:
数据库索引总结_第5张图片

根据叶子节点的内容,索引类型分为主键索引和非主键索引:
1、主键索引的叶子节点存储的是Page,Page中是一个有序数组,通过二分法定位到对应的数据行。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
2、非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。

主键索引和普通索引查询的区别:
如果语句是 select * from T where ID=500,即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是 select * from T where k=5,即普通索引查询方式,则需要先搜索 k 索引树,得到 ID 的值为 500,再到 ID 索引树搜索一次。这个过程称为回表。

所以,基于非主键索引的查询需要多扫描一棵索引树。因此,在应用中应该尽量使用主键查询。

索引的类型

Normal:表示普通索引
Unique:表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为unique
Full Text: 表示全文搜索的索引。 FULLTEXT 用于搜索很长一篇文章的时候,效果比较好

索引的维护

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。

如果新插入的 ID 值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。而更糟的情况是,如果 R5 所在的数据页P1已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据Pn,然后P1上需要挪动部分数据到Pn。原本在P1上的数据现在分布在P1和Pn两个数据页上了,这个过程称为页分裂。在这种情况下,性能自然会受影响。

除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。

有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程

自增主键

自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,都不涉及到挪动其他记录,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。

除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?

由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,则只要 4 个字节,如果是长整型(bigint)则是 8 个字节。

显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。

索引的重建

索引可能因为删除,或者页分裂等原因,导致数据页有空洞,重建索引的过程会创建一个新的索引,把数据按顺序插入,这样页面的利用率最高,也就是索引更紧凑、更省空间。

在非聚簇索引上使用索引重建,可以达到省空间的目的。比如:

alter table T drop index k;
alter table T add index(k);

但是,重建主键索引的过程需要注意。不论是删除主键还是创建主键,都会将整个表重建。所以其他维护好的索引就白维护了。主键索引的重建,可以用这个语句代替 :

alter table T engine=InnoDB;

你可能感兴趣的:(索引mysql)