深入浅出MySQL索引(一)

1 问题引入

首先,笔者在这里明确一点:处理数据都是在内存中进行的,考虑得都是内存中的时间复杂度。
如果我们要操作的数据集非常大,大到内存已经没法处理了怎么办?比如数据库表里面上千万条记录。在这种情况下,对数据的处理需要不断从硬盘等存储设备中调入或者调出内存页面。
一旦涉及到外部存储设备,关于时间复杂度的计算就会发生很大变化。试想一下,为了要在一个拥有几十万个文件的磁盘里面中查找一个文本文件,读取上万次呢,还是读取几十次?为了减少对外存设备的访问次数,我们就需要新的数据结构来处理这样的问题。

2 B树(简单介绍)

于是,B树就由此产生。B树一种平衡的多路查找树,节点中最大的孩子数目成为B树的阶。
在B树上查找的过程是一个顺时针查找结点和在结点中查找关键字的交叉过程。
4阶B树
举个例子,我们要查找数字7,首先从外存(比如硬盘中)读取得到根节点3,5,8三个元素,发现7不在当中,但在5和8之间。因此就通过A2在读取外存的6,7结点,查找到所要的元素。
如果内存与外存交换数据次数频繁,会使查询效率大大降低,那么B树怎么就可以做到减少次数呢?
补充一个知识点:外存是将所有的信息分割相等大小的页面,每次硬盘读写的都是一个或者多个完整的页面,对于一个硬盘来说,一页的长度可能是211或者214个字节。
在一个典型的B树应用(MySQL)中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因为我们会对B树的阶数(N)进行调整,使得B树的阶数硬盘存储的大小相匹配。以InnoDB的一个整数字段索引为例,这个N差不多是1200,当这棵树的高度是3(假设root的高度为1)的时候,就可以存1200的三次方,这已经是17亿了。考虑到根节点的数据块总是在内存里面,一个10亿行的表上整数字段的索引,查找一个值最多只需要访问2次硬盘。其实树的第二层也有很大的概率在内存中,那么访问硬盘的次数就更少了。

3 InnoDB的索引模型

3.1 B+树在InnoDB中的应用

在 InnoDB 中,表都是根据主键顺序以索引的形式存放的,这种存储方式的表称为索引组织表。InnoDB 使用了 B+ 树索引模型,所以数据都是存储在 B+树中。
每一个索引在 InnoDB 里面对应一棵 B+ 树。
假设,我们有一个主键列为 ID 的表,表中有字段 k,并且在k上有索引。

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

表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例图如下:
InnoDB模型描述
从图中不难看出,根据叶子节点的内容,索引类型分为主键索引和非主键索引。
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
提一个问题:基于主键的索引和普通索引有什么区别吗?

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

也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。

3.2 索引维护

B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护。以上面这个图为例,如果插入新的行id值为 700,则只需要在R5的记录后面插入一个新记录。如果新插入的id值为 400,就相对麻烦了,需要逻辑上挪动后面的数据,空出位置。
而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为页分裂。这种情况下,性能自然会受影响。
除了性能外,页分裂操作还影响数据页的利用率。原本放在一个页的数据,现在分到两个页中,整体空间利用率降低大约 50%。
当然,有页分裂,就有页合并。 当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
注意:索引不宜过多,根据业务情况,适当建立索引。实际使用中,最多不要超过6个。

3.3 自增id的优势

3.3.1 从性能角度

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

3.3.1 从存储的空间角度

假设你的表中确实有一个唯一字段,比如字符串类型的统一社会信用代码,那应该用统一社会信用代码做主键,还是自增id呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用统一社会信用代码做主键,那么每个二级索引的叶子节点占用约20个字节;如果用整型做主键,则只要4个字节;如果是长整型(bigint),需要8个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
综上所述,从性能和存储空间考虑,自增id往往是最优的原则。
那么,有什么场景是业务字段作为主键的呢?不妨考虑以下几点:

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

哈哈,细心的读者一定发现了,这就是典型的KV场景。

4 小结

B+树能够很好地配合磁盘的读写特性,减少单次查询的磁盘访问次数。由于 InnoDB 是索引组织表,一般情况建议创建一个自增主键,这样非主键索引占用的空间最小。

发布了16 篇原创文章 · 获赞 5 · 访问量 3295

猜你喜欢

转载自blog.csdn.net/qq_32573109/article/details/101202934