假设有这样一张表,字段 k 上的值不重复。想要在 k 这个字段上建立索引,从性能角度考虑,应该选择唯一索引还是普通索引呢?那么依据又是什么呢?
一、查询过程
"select id from T where k = 5"
这个语句。在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点。通过叶子节点获取到数据页后,然后可以认为数据页内部通过二分法来定位记录。
- 对于普通索引来说,查找到满足条件的第一个记录后,还需要查找下一个记录,直到碰到第一个不满足
k=5
条件的记录 - 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
InnoDB 的数据是按数据页为单位来读写的。每个数据页的大小默认为 16KB。看起来普通索引比唯一索引,多做了一次指针查找和数值判断。但同时,在最坏情况下,如果 k=5
这个记录刚好是这个数据页的最后一个记录,那么要取下一个记录,必须读取下一个数据页。这个性能损耗会多一点。
二、更新过程
1. 认识 change buffer
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。
并且 change buffer 是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操作写到原数据页,得到最新结果的过程被称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭的过程中,也会执行 merge 操作。
因此,change buffer 可以让数据库更新操作,减少读磁盘,SQL 语句的执行速度得到提升。
2. 使用 change buffer
唯一索引的更新不会使用 change buffer,因为对于唯一索引,更新操作需要判断这个操作是否违反唯一性约束。比如要插入 k=5
这个记录,就要先判断表中是否已经存在 k=5
这个记录了。而这必须要将数据页从磁盘读入内存中才能判断。如果都已经读入内存了,直接更新内存即可,没有必要再使用 change buffer 了。
那么针对要更新的这个记录的数据页是否在内存中。可以分为两种情况:数据页在内存中;数据页不在内存中。数据页在内存中时,直接操作内存,普通索引比唯一索引只是多了一次指针寻址和数据判断。
当数据页不在内存中时:
- 唯一索引,需要将数据页读入内存,判断有没有冲突,然后插入或更新这个值,语句执行结束
- 普通索引,则是将这个记录存在 change buffer 中,语句执行就结束了
将数据从磁盘读入内存涉及到随机 IO 的访问,这是数据库中成本最高的操作之一。change buffer 的使用则可以减少随机磁盘访问,所以对更新操作的性能提高很明显。
3. change buffer 的使用场景
在 merge 的时候是真正进行数据更新的时刻,也就是将 change buffer 中的数据合并到内存的数据页中。而 change buffer 的主要目的就是将记录的变更动作进行缓存。所以在一个数据页做 merge 之前,change buffer 记录的变更越多,也就是这个数据页上要更新的次数越多,收益就越大。
因此,对于写多读少的业务来说,数据页在写完以后马上被访问的概率比较小,此时 change buffer 的使用效率最好。
反过来,如果一个业务,在写完之后马上会做查询,这个过程就相当于,这个记录先写在 change buffer 中,然后又马上触发了 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代码。所以这种 “写完即读” 的业务模式,change buffer 反而起到了副作用。
4. merge 的执行流程
- 从磁盘读入数据页到内存中,此时数据页还是旧版本的数据
- 从 change buffer 中找出关于这个数据页的记录(可能有多个),然后依次进行合并,得到新版本的数据页
- 写 redo log,这个 redo log 包含了数据页的变更和 change buffer 的变更。
整个 merge 过程就结束了。这时候,内存中的数据页和内存中 change buffer 对应的磁盘空间都没有修改,属于脏页。之后通过配置的刷新机制会刷回这些磁盘物理页面。
5. change buffer 丢数据
如果某次写入使用了 change buffer 机制,之后主机异常重启,是否会丢失 change buffer 和数据呢?
答案是:不会丢失。change buffer 虽然只更新内存,但是在事务提交的时候,我们把 change buffer 的操作也记录到了 redo log 中了。所以崩溃恢复的时候,change buffer 也能找回来。
三、总结
因此,对于普通索引和唯一索引。这两种索引在查询能力上一般可以认为是没有区别的。主要考虑的是更新时的性能影响。一般尽量选择普通索引。
如果是 “写完即读” 的场景,那么应该关闭 change buffer。
在实际环境中,普通索引 和 change buffer 的配合使用,对于数据量大的表的更新操作,性能优化是比较明显的。尤其使用的如果是机械硬盘,并且是 “写多读少” 的业务场景,那就尽量使用普通索引,并且把 change buffer 尽量开大,以确保高效的写入性能。