查询过程
假设一个ID为主键的表,表中还有一列k,k上有索引。
mysql> create table T(
id int primary key,
k int not null,
name varchar(16),
index (k)
)engine=InnoDB;
查询的语句为:
select id from T where k=5;
表中 R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下
这个查询语句在索引树上查找过程:
显示通过B+树从树根开始,按层搜索到叶子节点。
- 对于普通索引而言,查找到满足条件的第一个记录(5,500)后,需要查询下一个记录,直到找到第一个不满足k=5条件的记录。
- 对于唯一索引而言,由于索引定义了唯一性,找到第一个满足条件的记录,就会停止检索。
这两者的不同带来的性能差距极其小,因为InnoDB是按数据页为单位来读写的。要读取一条记录时,不是将这个记录本身从磁盘读取出来,而是以页为单位,将其整体读入内存。在InnoDB中,每个数据页的大小默认是16K。
由于引擎按页读取,所以找到k=5的记录的时候,它所在的数据也都在内存了,对于普通所以而言,要多做的“查找和判断下一条记录”的操作,变成了只需要一次指针寻找和一次计算。
当然,如果恰好k=5这个记录刚好是这个数据页的最后一条记录,要取下一个记录,必须读取下一个数据页。
对于整型数据,一个数据页可以存放近千个key,出现上述这种情况的概率很低,而计算平均性能差异时,这个操作对于目前的CPU而言可以忽略不计。
更新过程
change buffer
当需要更新一个数据页时,如果数据页在内存就直接更新,如果这个数据页还没有在内存中,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。在下次查询的时候如果要访问这个数据页,将这个数据页读入内存,然后执行change buffer中与这个页有关的操作。
虽然名字叫作 change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。
将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。 除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge 操作。
显然,将更新操作先记录在 change buffer,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool 的,所以这种方式还能够避免占用内存,提高内存利用率。
什么条件下可以使用change buffer?
唯一索引,所有的更新操作都需要判断当前操作是否违反唯一性约束,如果k是唯一索引,要插入(4,400)这个记录,就需要先判断表中是否存在k=4的记录,而这必须要将数据页读入内存才能判断,此时已经读入到内存了,直接更新内存会更快,没必要使用change buffer了。
因此:唯一索引的更新不能使用change buffer,实际上只有普通索引可以使用。
change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。
在表中插入一条新纪录(4,400)的话,InnoDB的处理流程是怎样?
情况一:这条记录要更新的目标在内存中。
InnoDB处理流程如下:
- 对于唯一索引而言,找到3和5之间的位置,判断到没有冲突,插入这个值,语句执行结束。
- 对于普通索引而言,找到3和5之间的位置,插入这个值,语句执行结束。
结论:这种情况普通索引和唯一索引对更新语句的性能影响的差别,只是一个判断,只会耗费微小的CPU时间。
情况二:这个记录要更新的目标页不在内存中
InnoDB处理流程如下:
- 对于唯一索引而言,需要将数据页读入内存,判断到没有冲突,插入这个值,执行结束。
- 对于普通索引而言,将更新记录存在change buffer,语句执行结束了。
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一,change buffer因为减少了随机磁盘的访问,所以对于更新性能的提升是会很明显的。
所以上述情况二普通索引的性能会更高。
change buffer使用场景
在普通索引下,change buffer对更新过程有加速作用,但是change buffer不适用于唯一索引。
change buffer的目的是将记录的变更动作先缓存下来,merge的时候才是真正进行数据更新的时刻,所以在一个数据页做merge之前,change buffer记录的变更越多,说明收益越大。
结论:对于写多读少,且写完后被访问到的概率较低的场景,使用change buffer的效果最好。常见的业务类似账单类,日志类的系统。
如果一个业务在更新后,需要立马做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。
索引选择和实践
唯一索引和普通索引,两类索引在查询能力上是没差别的,主要考虑的是对更新性能的影响。所以,建议尽量选择普通索引。
change buffer 和 redo log
现在,我们要在表上执行这个插入语句:
mysql> insert into t(id,k) values(id1,k1),(id2,k2);
这里,我们假设当前 k 索引树的状态,查找到位置后,k1 所在的数据页在内存 (InnoDB buffer pool) 中,k2 所在的数据页不在内存中。如下图所示是带 change buffer 的更新状态图。
这条更新语句,涉及4部分:
- 内存
- redo log(id_log_fileX)
- 数据表空间(t.ibd)
- 系统表空间(ibdata1)
这条更新语句做如下操作(如托中的数字顺序)
- 1.Page1在内存中,直接更新内存。
- 2.Page2没有在内存中,就在内存的change buffer区域,记录“我要往Page2插入一行”这个信息。
- 3.将上述两个动作记入 redo log中(图中的3和4)
做完上面这些,事务就可以完成了,所以更新语句的成本很低,就是写了两处内存,然后写了一处磁盘(两次操作合在一起写了一次磁盘),并且是顺序写的。
后续执行查询语句:
select * from t where k in (k1, k2)
如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redo log(ib_log_fileX)无关了。所以,在图中就没画出这两部分。
- 读 Page 1 的时候,直接从内存返回。有几位同学在前面文章的评论中问到,WAL 之后如果读数据,是不是一定要读盘,是不是一定要从 redo log 里面把数据更新以后才可以返回?其实是不用的。你可以看一下图 3 的这个状态,虽然磁盘上还是之前的数据,但是这里直接从内存返回结果,结果是正确的。
- 要读 Page 2 的时候,需要把 Page 2 从磁盘读入内存中,然后应用 change buffer 里面的操作日志,生成一个正确的版本并返回结果。
可以看到,直到需要读 Page 2 的时候,这个数据页才会被读入内存。
所以,如果要简单地对比这两个机制在提升更新性能上的收益的话,redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。