04 | 深入浅出索引(上)

143 阅读5分钟

索引的常见模型

索引的出现就是为了提高数据查询的效率。 用于提高读写效率的数据结构有很多,常见的几种:

  • 哈希表
  • 有序数组
  • 搜索树

哈希表

  • 适合等值查询场景,类似Memcached以及其他NoSQL引擎
  • 不适合做区间查询,哈希表结构不是有序的。

哈希表是一种以 键-值(key-value)存储数据的结构,哈希的思路简单,把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置。 不可避免的,多个key经过哈希函数的计算,会出现同一个值的情况,处理这种情况的一种方法是拉出一个链表。

现在维护着一个身份证信息和姓名的表,需要根据身份证号查找对应的名字,对应的哈希索引的示意图如下所示:

image.png 图中,user2和user4根据身份证号算出来的值都是N,此时若要查询ID_card_n2对应的名字,步骤如下:先将ID_card_n2通过哈希函数算出N,然后按顺序遍历,找到user2。
哈希表结构不是有序的,所以做区间查询是很慢的。
哈希表这种结构适用于等值查询的场景,比如Memcached以及其他NoSQL引擎。

有序数组

  • 适合等值查询(二分法)
  • 适合范围查询
  • 缺点:更新操作成本过高

有序数组在等值查询和范围查询场景中性能都很优秀。 image.png 假设身份证号无重复,这个数组就是按照身份证号递增顺序保存,如果要查询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的身份证号,退出循环。

但是,更新操作时,必须挪动后面所有的记录,成本过高。

有序数组索引只适用于静态存储结构。

二叉搜索树

image.png

二叉搜索树的特点是:每个节点的左儿子小于父节点,父节点又小于右儿子。这样如果你要查 ID_card_n2 的话,按照图中的搜索顺序就是按照 UserA -> UserC -> UserF -> User2这个路径得到。这个时间复杂度是 O(log(N))。

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

InnoDB的索引模型

在InnoDB中,表都是根据主键顺序以索引的形式存放的, 这种存储方式的表称为索引组织表。 InnoDB使用了B+树的索引模型,所以数据都是存储在B+树中。

每一个索引在InnoDB中对应一颗B+树。

假设有如下表,表有3个字段id、k、name,其中ID加了主键索引,k加了普通索引。

mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k))engine=InnoDB;

表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下:

image.png

上图,根据叶子节点的内容,索引分为主键索引非主键索引

  • 主键索引(聚簇索引clustered index):叶子节点存的是整行数据。
  • 非主键索引(二级索引secondary index):叶子节点内容是主键的值。

主键索引和普通索引的查询过程有什么区别?

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

索引维护

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。

页分裂

如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响

页合并

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

自增主键的优点以及适用场景

自增主键:插入新记录的时候可以不指定 ID 的值,系统会获取当前 ID 最大值加 1 作为下一条记录的ID值。

【自增主键的优点】

  • 优点1:避免了叶子节点的分裂:自增主键的插入数据模式,每次插入一条新记录,都是追加操作,不涉及挪动其他记录,也不会触发叶子节点的分裂。
  • 优点2:节约普通索引占用空间,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。(虽然身份证号(即业务字段)也可以作为主键,但是占用的字节太大,普通索引的叶子节点是主键的值,会导致索引占用空间过大)

结论:从性能和存储空间方面考虑,自增主键往往是更合理的选择。

什么场景适合用业务字段做主键?

比如有的业务场景需求是这样:

  • 1.只有一个索引
  • 2.该索引必须是唯一索引

典型的KV场景,由于没有其他索引,无需考虑其他索引的叶子节点大小的问题。