@adamhand
2019-02-23T19:05:54.000000Z
字数 2973
阅读 964
先看一个问题,假如现在有一个市民系统,市民的唯一标志是身份证号,所以会有很多根据身份证号查询信息的需求。由于身份证号占用空间比较大,所以最好不要将其设置为主键索引,因为二级索引的携带的数据是主键,会导致二级索引占用空间很大。那么,现在的问题是,要讲身份证号设置为唯一索引还是普通索引呢?
以下面的图为例分析一下普通索引和唯一索引的查询效率和更新效率。
假如要执行的查询语句是:select id from T where k=5
,两种索引的查询过程如下:
这两个查询的区别就是普通索引可能要查找多条记录,而唯一索引只需要查询一条。实际上,这两种查询效率其实差别很小。原因如下:
InnoDB
的数据是按数据页为单位来读写的。当需要读一条记录的时候,是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。所以,当找到 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
的merge
的时候才是真正进行数据更新的时刻,而 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 pool
的 50%
。
普通索引和唯一索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,在业务能够保证索引不重复的前提下,将唯一索引改为普通索引可以提升效率,建议尽量选择普通索引。而在业务不能保证的情况下,还是以业务为先,在数据库增加唯一约束。
如果所有的更新后面,都马上伴随着对这个记录的查询,那么应该关闭 change buffer
。而在其他情况下,change buffer
都能提升更新性能。
这两个是比较容易搞混的概念,下面通过一个例子来区分一下。
如果要在表上执行以下语句:
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)
。这条更新语句做了如下的操作(按照图中的数字顺序):
Page 1
在内存中,直接更新内存;Page 2
没有在内存中,就在内存的 change buffer
区域,记录下“我要往 Page 2
插入一行”这个信息redo log
中(图中 3
和 4
)。图中的两个虚线箭头,是后台操作,不影响更新的响应时间。如果要使用select * from t where k in (k1, k2)
读出数据,流程如下图所示(假设读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1
)和 redo log(ib_log_fileX)
无关了。所以,图中不包含这两部分)。
Page 1
的时候,直接从内存返回。可以看到,WAL
之后如果读数据,并不是一定要读盘,即不一定要从 redo log
里面把数据更新以后才可以返回。上图 的这个状态,虽然磁盘上还是之前的数据,但是这里直接从内存返回结果,结果是正确的Page 2
的时候,需要把 Page 2
从磁盘读入内存中,然后应用 change buffer
里面的操作日志,生成一个正确的版本并返回结果。可以看到,直到需要读 Page 2
的时候,这个数据页才会被读入内存。所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log
主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer
主要节省的则是随机读磁盘的 IO 消耗。