[关闭]
@adamhand 2019-02-23T19:05:54.000000Z 字数 2973 阅读 964

09 | 普通索引和唯一索引,应该怎么选择?


先看一个问题,假如现在有一个市民系统,市民的唯一标志是身份证号,所以会有很多根据身份证号查询信息的需求。由于身份证号占用空间比较大,所以最好不要将其设置为主键索引,因为二级索引的携带的数据是主键,会导致二级索引占用空间很大。那么,现在的问题是,要讲身份证号设置为唯一索引还是普通索引呢?

例子分析

以下面的图为例分析一下普通索引和唯一索引的查询效率和更新效率。



查询效率

假如要执行的查询语句是:select id from T where k=5,两种索引的查询过程如下:

这两个查询的区别就是普通索引可能要查找多条记录,而唯一索引只需要查询一条。实际上,这两种查询效率其实差别很小。原因如下:

更新效率

首先需要了解一个change buffer的概念。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,InooDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。

所以, Change buffer的主要作用是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果。

MySQL5.5之前的版本中,由于只支持缓存insert操作,所以最初叫做insert buffer,只是后来的版本中支持了更多的操作类型缓存,才改叫change buffer

change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。

change buffer的使用场景

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffer 了。

因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。

那么,是不是所有普通索引都适合使用change buffer呢?并不是。

由于change buffermerge 的时候才是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多),收益就越大。因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。

反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。

因此,对于普通索引来说,更新操作由于能够使用change buffer,减少了磁盘IO,所以能够取得比唯一索引更好的效果。

另外,需要补充的一点,change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool50%

索引选择实践

普通索引和唯一索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,在业务能够保证索引不重复的前提下,将唯一索引改为普通索引可以提升效率,建议尽量选择普通索引。而在业务不能保证的情况下,还是以业务为先,在数据库增加唯一约束

如果所有的更新后面,都马上伴随着对这个记录的查询,那么应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。

chage buffer和redo log

这两个是比较容易搞混的概念,下面通过一个例子来区分一下。

如果要在表上执行以下语句:

  1. mysql> insert into t(id,k) values(id1,k1),(id2,k2);

假设当前 k 索引树的状态,查找到位置后,k1 所在的数据页在内存 (InnoDB buffer pool) 中,k2 所在的数据页不在内存中。如下图所示是带 change buffer 的更新状态图。



可以看到,这天语句涉及了四个部分:内存redo log(ib_log_fileX)数据表空间(t.ibd)系统表空间(ibdata1)。这条更新语句做了如下的操作(按照图中的数字顺序):

图中的两个虚线箭头,是后台操作,不影响更新的响应时间。如果要使用select * from t where k in (k1, k2)读出数据,流程如下图所示(假设读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。所以,图中不包含这两部分)。



可以看到,直到需要读 Page 2 的时候,这个数据页才会被读入内存。所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

参考

MySQL · 引擎特性 · Innodb change buffer介绍
选择唯一索引还是普通索引?

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注