MySQL索引(二):索引选择

时间:2024-12-05 19:42:32

在不同的业务场景下, 应该选择普通索引, 还是唯一索引?下面对其进行分析...

Change Buffer


InnoDB的数据是按页为单位读写的。当读一条记录时,并不是将这个记录本身从磁盘读出来,而是以页为单位,将记录所在的页整体读入内存。页大小默认是16KB,存放页的内存区域称为InnoDB Buffer Pool;

Change Buffer是InnoDB Buffer Pool中的一块内存区域,因此不能无限增大。change buffer的大小,可以通过 innodb_change_buffer_max_size 参数来动态设置。这个参数设置为50的时候, 表示change buffer的大小最多只能占用buffer pool的50%。

问1:Change Buffer有什么用处?

可以提高更新性能。

当需要更新一个数据页时,如果数据页在内存中就直接更新;反之,在不影响数据一致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了;在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中与这个页有关的操作,以保证数据逻辑的正确性。

显然, 如果能够将更新操作先记录在change buffer, 减少读磁盘, 语句的执行速度会得到明显的提升。 而且, 数据读入内存是需要占用buffer pool的, 所以这种方式还能够避免占用内存, 提高内存利用率。

问2:Change Buffer数据会不会被持久化?

change buffer在内存中有拷贝,也会被写入到磁盘上,放在系统表空间文件中。

问3:什么是Change Buffer的merge操作?

将change buffer中的操作应用到旧数据页,得到新数据页的过程称为merge。

Merge的触发条件包括:

1)访问这个数据页。

2)后台线程会定期merge。

3)正常关闭(shutdown)数据库。

问4:使用Change Buffer有哪些意义?

1)减少磁盘IO:将更新操作先记录在change buffer,减少读磁盘,语句的执行速度会得到明显的提升。

2)节约内存:减少读入内存的数据页,仅记录变更,提高内存利用率。

问5:Change Buffer有哪些使用约束?

仅普通索引可以使用。唯一索引所有的更新操作都要先判断这个操作是否违反唯一性约束,因而需要将数据页读入内存。

问6:Change Buffer的使用场景有哪些?

只有普通索引可以使用。

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

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

此外,因为merge的时候是真正进行数据更新的时刻, 而change buffer的主要目的就是将记录的变更动作缓存下来, 所以在一个数据页做merge之前,change buffer记录的变更越多(也就是这个页面上要更新的次数越多) ,收益就越大。

问7:普通索引的所有场景,使用change buffer都可以起到加速作用么?

仅适用于读多写少的场景。

对于写多读少的业务来说, 页面在写完以后马上被访问到的概率比较小, 此时change buffer的使用效果最好。 这种业务模型常见的就是账单类、 日志类的系统。

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

Change Buffer补充:

1)change buffer和数据页一样,也是物理页的一个组成部分,数据结构也是一颗B+树,这棵B+树放在系统表空间中,默认ibdata1中;

2)change buffer写入系统表空间机制和普通表的脏页刷新到磁盘是相同的机制——Checkpoint机制;

3)之所以change buffer要写入系统表空间,是为了保证数据的一致性,change buffer做修改时需要写redo,在做恢复时需要根据redo来恢复change buffer,若是不把change buffer写入系统表空间,也就是不进行持久化,那么在change buffer写入内存后掉电,则无法进行数据恢复。这样也会导致索引中的数据和相应表的相应列中的数据不一致;

4)change buffer 写入到了系统表空间,purge的时候会先查询change buffer里对应的记录(即检查change buffer中的记录更新操作是否done),然后进行purge,因为change buffer B+树的key是表空间ID,所以查询根据表空间ID查询change buffer会很快;

5)merge的执行过程:

  • 从磁盘读入数据页到内存(老版本的数据页);
  • 从change buffer里找出这个数据页的change buffer 记录(可能有多个),依次应用,得到新版数据页;
  • 写redo log,这个redo log包含了数据的变更和change buffer的变更;
  • merge流程(新版本数据页)并不会直接写磁盘,而是等待变更后的内存页通过刷脏后落盘;

注1:change buffer和merge后的新版本数据页都会被写到redo log。change buffer写入redo log主要是为了防止change buffer写入内存后掉电;merge后的新版本数据页写入redo log主要是为了保证数据的一致性。

注2:change buffer节省的是随机读磁盘的IO次数,写redo log(孔乙己粉板)节省的是随机写IO次数。

普通索引和唯一索引读写性能分析


假设有如下表结构:

-- 表T结构
create table T(
   id int primary key, 
   k int not null, 
   name varchar(16),
   index (k)
) engine=InnoDB;

查询操作

SQL语句:select id from T where k=5;

  • 若k为普通索引,则查找到满足条件的第一个记录(5,500)后,需要继续查找下一个记录,直到碰到第一个不满足k=5条件的记录;
  • 若k为唯一索引,由于索引定义了唯一性,则查找到第一个满足条件的记录后,就会停止继续检索;

注:对于非唯一索引来说,会多做一次查找和计算;但性能影响很小;

问:为什么非唯一索引多做一次查找和计算,性能影响很小?

因为引擎是按页读写的, 所以说, 当找到k=5的记录的时候, 它所在的数据页就都在内存里了。那么, 对于普通索引来说, 要多做的那一次“查找和判断下一条记录”的操作, 就只需要一次指针寻找和一次计算。

当然, 如果k=5这个记录刚好是这个数据页的最后一个记录, 那么要取下一个记录, 必须读取下一个数据页, 这个操作会稍微复杂一些。

但是, 对于整型字段, 一个数据页可以放近千个key, 因此出现这种情况的概率会很低。 所以, 计算平均性能差异时, 仍可以认为这个操作成本对于现在的CPU来说可以忽略不计。

更新操作

SQL语句:insert into T values( 4, 400, xxx );

场景1:假设更新的目标页在内存中;

  • 若k为普通索引,找到3和5之间的位置,插入这个值,语句执行结束;
  • 若k为唯一索引,找到3和5之间的位置,若没有冲突,则插入这个值,语句执行结束;(唯一索引的所有更新操作都需要把数据页更新到内存中,以判断当前操作是否违反唯一性约束)

场景2:更新的目标也不在内存中;

  • 若k为普通索引,将更新记录在change buffer,语句执行结束;
  • 若k为唯一索引,将数据页读入内存,若没有冲突,则插入这个值,语句执行结束;

唯一索引更新过程中,需要保证更新操作的唯一性,因此需要把数据页读入内存;而对于普通索引而言,无需把数据页读入内存,所以普通索引的更新性能要好于唯一索引;

注:基于普通索引和唯一索引的特性,在使用二者时需注意一下两点:

  • 对于不需要DB保证唯一性约束的二级索引,尽可能使用普通索引;
  • 对于更新数据后,要马上读取数据的场景,要关闭change buffer。而在其它情况下change buffer都能提升性能。

Change Buffer与Redo Log(WAL)的区别


Change Buffer与Redo Log(主要是指WAL机制)的区别主要表现在两个方面:更新、读取,下面分别对其展开介绍。

更新

更新操作:insert into t(id,k) values(id1,k1),(id2,k2);

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

分析这条更新语句,会发现它涉及4个部分:

  • 内存
  • redo log(ib_log_fileX)
  • 数据表空间(t.ibd)
  • 系统表空间(ibdata1)

更新过程如下:

1)Page 1在内存中,直接更新内存;(写内存)

2)Page 2不在内存中,则在change buffer中记录操作;(写内存)

3)将上述两个动作记入redo log中;(两次操作合在一起写了一次磁盘,而且是顺序写)

同时,图中的两个虚线箭头,是后台操作,不影响更新的响应时间。

问1:更新过程步骤2操作,change buffer一开始是写内存的,如果此时掉电重启,会不会导致change buffer丢失呢?

答:虽然是只更新内存, 但是在事务提交的时候, 把change buffer的操作也记录到redo log里了, 所以崩溃恢复的时候, change buffer也能找回来。(change buffer的落盘机制与数据页相似,也是 No Stale + No Force,因而未提交的事务,对应的change buffer回滚;已提交的事务对应的change buffer通过binlog + redo log恢复。)

问2:No Stale + No Force是什么?

No Steal + No Force是一种数据库管理策略的组合,具体涉及事务处理时数据更新到磁盘的时机和方式。在这种策略组合下:

  • No Steal:指的是不允许一个未提交(uncommitted)的事务将修改更新到磁盘。这意味着,在事务提交之前,其所做的任何修改都不会被持久化到磁盘上。这种策略有助于确保数据的一致性,因为如果在事务提交之前系统崩溃,未提交的修改不会影响到磁盘上的数据。
  • No Force:表示事务在提交(committed)之后可以不立即将更新持久化到磁盘。这种策略允许系统缓存多个更新,然后批量地将它们持久化到磁盘,从而减少了磁盘操作的次数,提高了写操作的效率。然而,这也带来了风险,因为如果在批量持久化之前系统崩溃,已经提交的事务数据可能会丢失。

结合这两种策略,“No Steal + No Force”提供了一种权衡数据一致性和系统性能的方案。它确保了未提交的事务修改不会被写入磁盘,同时允许已提交的事务修改在稍后的时间点进行批量持久化。然而,这种策略也增加了数据丢失的风险,特别是在系统崩溃或故障的情况下。因此,在采用这种策略时,通常需要配合其他机制(如日志记录)来确保数据的完整性和可恢复性。

注:No Steal + No Force更多关注于数据更新的时机和方式,而两阶段提交则关注于如何在分布式系统中保证事务的一致性。

读取

读取操作:select * from t where k in (k1,k2);

假设读语句发生在更新语句后不久,内存中的数据都还在,未刷脏;

查询过程如下:

1)读Page 1的时候,直接从内存返回。因为WAL之后如果读数据,不一定要读盘,可能直接从内存返回结果;

2)读Page 2的时候,需要把Page 2从磁盘读入内存中,然后应用change buffer里面的操作日志,生成一个正确的版本并返回结果;

上述过程即为page页的Merge过程;

注: merge后的内存页并不会直接写磁盘,而是等待后续的刷脏落盘;

问:Change Buffer和Redo Log在提升更新性能上的收益区别是什么?

redo log(WAL)主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的是随机读磁盘的IO消耗。