01、索引到底是怎么回事?
前面两篇说到了索引原理即数据结构,同时还讲到了其索引原则。那么查询一条SQL语句到底是怎么执行的呢?或者说除了主键索引,其他的索引到底是怎么一回事?今天的这篇主要任务就是理清这个关系,然后说说怎么优化创建的索引?这个很重要,绝对对你的工作有很大帮助,希望能细心的体会,并且深入理解,那么就开始吧。
先来了解概念,如下:
主键索引的叶子节点存的是整行数据。在 InnoDB 里,主键也被称为聚簇索引(clustered index)。
非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index)。
根据上面的索引结构说明,我们来讨论一个问题:基于主键索引和普通索引的查询有什么区别?
如果语句是 ,下面这条:
SELECT * from userinfo where id= 655;
即主键查询方式,则只需要搜索 ID 这棵 B+ 树;
如果语句是,下面这条:
SELECT * from userinfo where name= 'aaron';
即普通索引查询方式,则需要先搜索name 索引树,得到 ID 的值为 655,再到 ID 索引树搜索一次。这个过程称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
索引维护
先从问题开始入手考虑,索引维护这个主题。
为什么公司规定主键ID一般是自增长,且是整型?
性能方面考虑:
B+ 树为了维护索引有序性,在插入新值的时候需要做必要的维护.如果插入新的行 ID 值为 700,则只需要在 R5 的记录后面插入一个新记录。如果新插入的 的 ID 值为 400,就相对麻烦了。需要逻辑上挪动后面的数据,空出位置。
而更糟的情况是,如果 R5 所在的数据页已经满了,根据 B+ 树的算法,这时候需要申请一个新的数据页,然后挪动部分数据过去。这个过程称为页分裂。在这种情况下,性能自然会受影响。
当然有分裂就有合并。当相邻两个页由于删除了数据,利用率很低之后,会将数据页做合并。合并的过程,可以认为是分裂过程的逆过程。
自增主键的插入数据模式,正符合了我们前面提到的递增插入的场景。每次插入一条新记录,都是追加操作,也不会触发叶子节点的分裂。而有业务逻辑的字段做主键,则往往不容易保证有序插入,这样写数据成本相对较高。
存储方面考虑:
除了考虑性能外,我们还可以从存储空间的角度来看。假设你的表中确实有一个唯一字段,比如字符串类型的身份证号,那应该用身份证号做主键,还是用自增字段做主键呢?
由于每个非主键索引的叶子节点上都是主键的值。如果用身份证号做主键,那么每个二级索引的叶子节点占用约 20 个字节,而如果用整型做主键,如果是长整型(bigint)则是 8 个字节。
显然,主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小。
所以,从性能和存储空间方面考量,自增主键往往是更合理的选择。
有没有什么场景适合用业务字段直接做主键的呢?
比如,有些业务的场景需求是这样的:
1.只有一个索引;
2.该索引必须是唯一索引。
这就是典型的 KV 场景。直接将这个索引设置为主键,可以避免每次查询需要搜索两棵树。
02、高性能的索引策略
高效地选择和使用索引有很多种方式,其中有些是针对特殊案列的优化方式,有些则是针对特定行为的优化。
1.独立的列
是指索引列不能是表达式的一部分,也不能是函数的参数。
错误的方式,肉眼可以看出,但是mysql并不能识别出。
SELECT id FROM userinfo where id + 1 = 2;
正确的方式是:
SELECT id FROM userinfo where id = 1;
2.前缀索引和索引选择性
有时候需要索引很长的字符串,这会让索引变得大且慢。一个策略是哈希索引。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效果。但这样会降低索引的选择性。索引的选择性是指,不重复的索引值(也称为基数)和数据表的记录总数(#T)的比值,范围从1/#T到1之间。索引的选择性越高则查询效率越高,因为选择性高的索引让mysql在查找时过滤掉更多的行。
3.多列索引
一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。
creat table t(
c1 int,
c2 int,
c3 int,
key(c1),
key(c2),
key(c3)
);
把"where 条件里的列都建上索引",这个是有点错误的想法,这个可能只是"一星"索引,其性能比起真正最优的索引可能差几个数量级。有时如果无法设计一个"三星"索引,那么不如忽略掉where子句,集中精力优化索引列的顺序,或者创建一个全覆盖索引。进而,引出"索引合并":
SELECT id FROM userinfo where age > 10 or sex ='M';
一般会使用全表扫描的,除非改写成如下的两个表查询union的方式:
SELECT id FROM userinfo where age > 10
union all
SELECT id FROM userinfo where sex ='M';
如果对 age和sex进行索引合并,还是一样的查询:
SELECT id FROM userinfo where age > 10 or sex ='M';
可能将会大大改善其结果。
索引合并策略有时候是一种优化的结果,但实际上更多时候说明了表上的索引建的很糟糕:
1.当出现服务器对多个索引做相交操作时(通常有多个AND条件),通常意味着需要一个包含所有相关列的多列索引,而不是多个独立的单列索引。
2.当服务器需要对多个索引做联合操作时(通常有多个or条件),通常需要耗费大量CPU和内存资源在算法的缓存、排序和合并操作上。特别是当其中有些索引的选择性不高,需要合并扫描返回的大量数据的时候。
4.选择合适的索引列顺序
在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列等等。所以,索引可以按照升序或者降序进行扫描。以满足精确符合顺序的order by、Group by 和 distinct 等子句的查询需求。
当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化where条件的查找。在这种情况下,这样的设计的索引确实能够最快地过滤出需要的行,对于where子句中只使用了索引部分前缀的查询来说选择性也更高。然而,性能不只是依赖于所有索引列的选择性(整体基数),也和查询条件的具体值有关,也就是和值的分布有关。
那么我们应该怎么选择组合索引字段的顺序呢?
select * from userinfo where id = 2 and customer_id = 584;
是应该创建一个(id,customer_id)索引还是应该颠倒一下顺序?可以跑一些查询来确定在这个表中值的分布情况,并却确定那个列的选择性更高。
先使用sum函数来看看where条件的分支对应的数据基数有多大:
select sum(id = 2 ), sum (customer_id = 584) from userinfo ;
结果为:
sum(id = 2 ): 7992 ; sum (customer_id = 584) : 30
根据前面的法则,应该将索引列 customer_id 放到前面,因为对应条件customer_id 数量更小。最后,尽管关于选择性和基数的经验法则值得去研究和分析,但一定要记住别忘记了where子句的排序、分组和范围条件等其他因素。
5.聚族索引
聚族索引并不是一种单独的索引类型,而是一种数据存储方式。当表有聚族索引时,它的数据行实际上存放在索引的叶子页中。
InnoDB将通过主键聚族数据,如果没有定义主键,InnoDB会选择一个唯一的非空索引代替。如果没有这样的索引,InnoDB会隐式定义一个主键来作为聚族索引。InnoDB只聚集在同一个页面中的记录。包含相邻键值的页面可能会相距甚远。
优点:
1.可以把相关的数据保存在一起。例如实现电子邮箱时,可以根据用户id来聚集数据,这样只需要从磁盘读取少数的数据页就能获取某个用户的全部邮件。如果没有使用聚族索引,则每封邮件都可能导致一次磁盘i/o。
2.数据访问更快。
3.使用覆盖索引扫描的查询可以直接使用页节点中的主键值。
缺点:
1.聚族数据最大限度地提高了i/o密集型应用的性能,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚族索引也就没什么优势了。
2.插入速度严重依赖插入顺序。按照主键的顺序插入是加载数据到InnoDB表中最快的方式。
3.更新聚族索引列的代价很高。
4.聚族索引可能导致全表扫描行,尤其是行比较稀疏,或者由于页分裂导致数据不连续的时候。
建议:最好尽可能使用单调增加的聚族键的值来插入新行。这样可以减少写入的时候分页相关操作。
6.覆盖索引
通常大家都会根据查询的where条件来创建合适的索引,不过这只是索引优化的一个方面。设计优秀的索引应该考虑到整个查询,而不是单单是where条件部分。如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就从称之为"覆盖索引".
优点:
1.索引条目通常远小于数据行大小,所以如果只需要读取索引,那mysql就会极大地减少数据访问量。
2.因为索引是按照列值顺序存储的,所以对于i/o密集型的范围查询会比随机从磁盘读取每一行数据的i/o要少得多。
3.由于InnoDB的聚族索引,覆盖索引对InnoDB表特别有用。InnoDB的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。
发起一个被被覆盖的查询,在EXPLAIN 的 Extra列可以看到"Using index" 的信息。那就可以使用这个索引做覆盖索引。
注意:Extra列 的 "Usering where" 是不可以做索引的。有可能是字段使用了like %%。
EXPLAIN SELECT ua.account_id,ua.user_id from user_account ua
可以根据account_id,user_id 做覆盖索引,也可以根据where条件所有值做覆盖索引。
7.使用索引扫描来做排序
mysql有两种方式可以生成有序的结果:通过排序操作;或者按照索引顺序扫描。如果EXPLAIN 出来的type列的值为"index",则说明mysql使用了索引扫描来做排序。
只要当索引的列顺序和order by子句的顺序完全一致,并且所有列的排序方向都一样时,mysql才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有当order by 子句引用的字段全部为第一个表时,才能使用索引做排序order by子句和查找型查询的限制是一样的。
对于有这样一个表 rental 在列(rental_date,inventory_id,customer_id)创建这样一个索引。
where rental_date='2019-04-05' order by inventory_id,customer_id ;
即使order by 子句不满足索引的最左前缀的要求,也可以用于查询排序,这是因为索引的第一列被指定为一个常数。
where rental_date = '2019-04-05' order by inventory_id desc;
可以利用查询为索引的第一列提供了常量条件,而使用第二列进行排序,将两列组合在一起,就形成了索引的最左前缀。
#下面这个也是最左前缀索引:
where rental_date = '2019-04-05' order by rental_date , inventory_id ;
#下面是一些不能使用索引做排序的查询:
#使用了两种不同的排序方向,但是索引列都是正序排序的:
where rental_date = '2019-04-05' order by inventory_id desc , customer_id asc;
#查询的order by 子句中引用了一个不在索引中的列:
where rental_date = '2019-04-05' order by inventory_id , staff_id;
#查询的where 和 order by 中的列无法组合成索引的最左前缀:
where rental_date = '2019-04-05' order by customer_id;
#查询在索引的第一列上是范围条件,所以mysql无法使用索引的其余列:
where rental_date > '2019-04-05' order by inventory_id ,customer_id;
#查询在inventory_id 列上有多个条件,对于排序来说,这也是一种范围查询:
where rental_date = '2019-04-05' and inventory_id in (1,2) order by customer_id ;
8. 冗余和重复索引
mysql 允许在相同列上创建多个索引,mysql需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。
creat table test(
ID int not null primary key,
A int not null,
UNIQUE(ID),
index(ID))
这个在创建的时候,就已经创建了重复的索引。
大多数情况下都不需要冗余索引,应该尽量扩展已有的索引而不是创建新索引。一般来说表中的索引越多插入速度会越慢(这个在项目中已经实验过了,数据迁移项目),同时增加新索引将会导致insert / update / delete 等操作的速度变慢,特别是当新增索引后导致达到了内存瓶颈的时候。
9.索引和锁
索引可以让查询锁定更少的行。如果你的查询从不访问哪些不需要的行,那么就会锁定更少的行。首先,虽然InnoDB的行锁效率很高,内存使用页很少,但是锁定行的时候,仍然会带来额外开销;其次,锁定超过需要的行会增加锁争用并减少并发性。
InnoDB只有在访问行的时候才会对其加锁。而索引能够减少InnoDB访问的行数,从而减少锁的数量。但只有当InnoDB在存储引擎层能够过滤掉所有不需要的行时才有效。
实战,案例一:
1. 支持多种过滤条件
比如有个表国家(country)列,这个列选择性通常不高,但可能会查询都会用到。sex的列选择性肯定也很低,但也会在很多查询中用到。所以考虑到使用的频率,还是建议在创建不同组合索引的时候将(sex,country)列作为前缀。
问题是,如果某个查询不限制性别,那么我们应该怎么做?
可以通过在查询条件中新增and sex in('m','f')来让mysql选择该索引。mysql能够匹配索引的最左前缀。但如果列有太多的值,就会让in()列表太长(这篇关于in语句与between对比文章),这样做久不太行了。
设计的原则:
考虑表上所有的选项。
当设计索引时,不要只考虑需要哪些索引,还需要考虑对查询进行优化。如果发现某些查询需要创建新索引,但是这个索引会降低另一些查询的效率,那么应该想一下是否能优化原来的查询。应该同时优化查询和索引以找到最佳的平衡。
接下来,需要考虑其他常见的where 条件的组合,并需要了解哪些组合在没有合适索引情况下会很慢。其实这个索引(sex,country)还可以加上(sex,country,region,age).
2.避免多个范围条件
什么是范围查询?
从explain的输出很难区分mysql是要查询范围值。但是我们可以从值的范围和多个等于条件来得出不同。
假设有这样一个语句:
where eye_color in('blue')
and hair_color in('black','red')
and sex in('m','f')
and last_online > DATE_SUB(NOW(),INTERVAL 7 DAY)
and age between 18 and 20
这个查询有一个问题,它有两个范围条件,last_online and age , mysql last_online 列索引 and age 列索引,但无法同时使用它们。这个是无法解决的,建议多创建几个组合索引,但是也不要创建太多,索引创建太多,可能导致插入很慢。
3.优化排序
例如,如果where 子句只有sex 列,如何排序?
对于那些选择性非常低的列,可以增加一些特殊的索引来做排序。 例如,可以创建(sex, rating) 索引用于下面的查询:
select * from profiles where sex = 'm' order by rating limit 10;
如果需要翻页:
select * from profiles where sex = 'm' order by rating limit 100000, 10;
无论如何创建索引,这种查询都是个严重的问题。因为随着偏移量的增加,mysql 需要花费大量的时间来扫描需要丢弃的数据。反范式化,预先计算和缓存可能是解决这类查询的仅有策略,一个更好的办法是限制用户能够翻页查询的数量,实际上这对用户体验的影响不大,因为用户很少真正在乎搜索结果的第10000页。
优化这类索引的另一个比较好的策略是使用延迟关联,通过使用覆盖索引查询返回需要的主键,在根据主键关联原表获得需要的行。这可以减少mysql扫描哪些需要丢弃的行数。
如何高效的利用(sex, rating)索引进行排序和分页:
select * from profiles inner join ( select (primary key) from profiles where x.sex = 'm' order by rating limit 100000, 10 ) as x using (primary key) ;
实战演练二:如何给字符串建立索引?
现在,几乎所有的系统都支持邮箱登录,如何在邮箱这样的字段上建立合理的索引?
create table SUser(
ID bigint unsigned primary key,
email varchar(64)
)engine=innodb;
一般的sql是这样的:
select f1, f2 from SUser where email='xxx';
如果 email 这个字段上没有索引,那么这个语句就只能做全表扫描。创建索引:
alter table SUser add index index1(email);
#或
alter table SUser add index index2(email(6));
第一个语句创建的 index1 索引里面,包含了每个记录的整个字符串;而第二个语句创建的index2 索引里面,对于每个记录都是只取前 6 个字节。
那么,这两种不同的定义在数据结构和存储上有什么区别呢?
最大的区别是:存储的数据变大。
email(6) 这个索引结构中每个邮箱字段都只取前 6 个字节,所以占用的空间会更小,这就是使用前缀索引的优势。但,这同时带来的损失是,可能会增加额外的记录扫描次数。
select id,name,email from SUser where email='[email protected]';
如果使用的是 index1(即 email 整个字符串的索引结构),执行顺序是这样的:
1.从 index1 索引树找到满足索引值是’[email protected]’的这条记录,取得 ID2 的值;
2.到主键上查到主键值是 ID2 的行,判断 email 的值是正确的,将这行记录加入结果集;
3.取 index1 索引树上刚刚查到的位置的下一条记录,发现已经不满足 email='[email protected]’的条件了,循环结束。
这个过程中,只需要回主键索引取一次数据。
如果使用的是 index2(即 email(6) 索引结构),执行顺序是这样的:
1.从 index2 索引树找到满足索引值是’fanron’的记录,找到的第一个是 ID1;
2.到主键上查到主键值是 ID1 的行,判断出 email 的值不是’[email protected]’,这行记录丢弃;
3.取 index2 上刚刚查到的位置的下一条记录,发现仍然是’fanron’,取出 ID2,再到 ID 索引上取整行然后判断,这次值对了,将这行记录加入结果集;
4.重复上一步,直到在 idxe2 上取到的值不是’fanron’时,循环结束。
在这个过程中,要回主键索引取 4 次数据,也就是扫描了 4 行。对于这个查询语句来说,如果你定义的 index2而是 email(7),也就是说取 email 字段的前 7 个字节来构建索引的话,即满足前缀’fanrong’的记录只有一个,也能够直接查到 ID2,只扫描一行就结束了。使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
那么问题来了,当要给字符串创建前缀索引时,有什么方法能够确定我应该使用多长的前缀呢?
实际上,我们在建立索引时关注的是区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。因此,我们可以通过统计索引上有多少个不同的值来判断要使用多长的前缀。
首先,你可以使用下面这个语句,算出这个列上有多少个不同的值:
select count(distinct email) as L from SUser;
#依次选取不同长度的前缀来看这个值,比如我们要看一下 4~7 个字节的前缀索引,可以用这个语句:
select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
前缀索引对覆盖索引的影响:
select id,email from SUser where email='[email protected]';
#要求返回 id 和 email 字段。
select id,name,email from SUser where email='[email protected]';
如果使用 index1(即 email 整个字符串的索引结构)的话,可以利用覆盖索引,从index1 查到结果后直接就返回了,不需要回到 ID 索引再去查一次。而如果使用 index2(即email(6) 索引结构)的话,就不得不回到 ID 索引再去判断 email 字段的值。
也就是说,使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。
那么有没有更好的方式呢?其它方式:
比如,我们国家的身份证号,一共 18 位,其中前 6 位是地址码,所以同一个县的人的身份证号前 6 位一般会是相同的。按照我们前面说的方法,可能你需要创建长度为 12 以上的前缀索引,才能够满足区分度要求。
但是,索引选取的越长,占用的磁盘空间就越大,相同的数据页能放下的索引值就越少,搜索的效率也就会越低。
如果我们能够确定业务需求里面只有按照身份证进行等值查询的需求还有没有别的处理方法呢?
第一种方式是使用倒序存储。如果你存储身份证号的时候把它倒过来存,每次查询的时候,你可以这么写:
select field_list from t where id_card = reverse('input_id_card_string');
第二种方式是使用 hash 字段。你可以在表上再创建一个整数字段,来保存身份证的校验码,时在这个字段上创建索引。
alter table t add id_card_crc int unsigned, add index(id_card_crc);
然后每次插入新记录的时候,都同时用 crc32() 这个函数得到校验码填到这个新字段。由于校验码可能存在冲突,也就是说两个不同的身份证号通过 crc32() 函数得到的结果可能是相同的,所以你的查询语句 where 部分要判断 id_card 的值是否精确相同。
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string'
这样,索引的长度变成了 4 个字节,比原来小了很多。使用倒序存储和使用 hash 字段这两种方法的异同点。
首先,它们的相同点是,都不支持范围查询。倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式。
1.从占用的额外空间来看,倒序存储方式在主键索引上不会消耗额外的存储空间,而 hash 字段方法需要增加一个字段。当然,倒序存储方式使用 4 个字节的前缀长度应该是不够的,如果再长一点,这个消耗跟额外这个 hash 字段也差不多抵消了。
2.在 CPU 消耗方面,倒序方式每次写和读的时候,都需要额外调用一次 reverse 函数,而 hash字段的方式需要额外调用一次 crc32() 函数。如果只从这两个函数的计算复杂度来看的话,reverse 函数额外消耗的 CPU 资源会更小些。
3.从查询效率上看,使用 hash 字段方式的查询性能相对更稳定一些。因为 crc32 算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近 1。而倒序存储方式毕竟还是用的前缀索引的方式也就是说还是会增加扫描行数。
总结:
1.直接创建完整索引,这样可能比较占用空间;
2.创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引。
3.倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区度不够的问题;
4.创建 hash 字段索引,查询性能稳定,有额外的存储和计算消算消耗,跟第三种方式一样,都不支持范围扫描。
03、选择索引依据是什么?
前面都是直接给出索引规则,这个应该怎么操作,这个应该使用什么索引。但是到底怎么会选择这个索引呢,主键索引?
假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。如果市民系统需要按照身份证号查姓名,就会执行类似这样的 SQL 语
select name from CUser where id_card = 'xxxxxxxyyyyyyzzzzz';
所以,你一定会考虑在 id_card 字段上建索引。由于身份证号字段比较大,我不建议你把身份证号当做主键,那么现在你有两个选择,要么给id_card 字段创建唯一索引,要么创建一个普通索引。如果业务代码已经保证了不会写入重复的身份证号,那么这两个选择逻辑上都是正确的。
1.从性能的角度考虑,你选择唯一索引还是普通索引呢?选择的依据是什么呢?
第一个节点以后说了主键索引和普通索引的区别。假设,执行查询的语句是:
SELECT * from userinfo where name='1000';
这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,然后可以认为数据页内部通过二分法来定位记录。
对于普通索引来说,查找到满足条件的第一个记录 (1000,5000)后,需要查找下一个记录,直到碰到第一个不满足 id=5 条件的记录。对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。
那么,这个不同带来的性能差距会有多少呢?答案是,微乎其微。
InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。
因为引擎是按页读写的,所以说,当找到 name='1000' 的记录的时候,它所在的数据页就都在内存里了。那么,对于普通索引来说,要多做的那一次“查找和判断下一条记录”的操作,就只需要一次指针寻找和一次计算。
2.更新过程
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
change buffer,实际上它是可以持久化的数据。也就是说,change buffer 在内存中有拷贝,也会被写入到磁盘上。将 change buffer 中的操作应用到原数据页,得到最新结果的过程称为 merge。除了访问这个数据页会触发 merge 外,系统有后台线程会定期 merge。在数据库正常关闭(shutdown)的过程中,也会执行 merge。
显然,如果能够将更新操作先记录在 change buffer.,减少读磁盘,语句的执行速度会得到明显的提升。而且,数据读入内存是需要占用 buffer pool的,所以这种方式还能够避免占用内存,提高内存利用率。
2.1 什么条件下可以使用 change buffer 呢?
对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。比如,要插入(4,400) 这个记录,就要先判断现在表中是否已经存在 name=4 的记录,而这必须要将数据页读入内存才能判断。如果都已经读入到内存了,那直接更新内存会更快,就没必要使用 change buffe了。
因此,唯一索引的更新就不能使用 change buffer,实际上也只有普通索引可以使用。再一起来看看如果要在这张表中插入一个新记录 (4,400) 的话,InnoDB 的处理流程是怎样的。
第一种情况是,这个记录要更新的目标页在内存中。这时,InnoDB 的处理流程如下:
1.对于唯一索引来说,找到 3 和 5 之间的位置,判断到没有冲突,插入这个值,语句执行结束;
2.对于普通索引来说,找到 3 和 5 之间的位置,插入这个值,语句执行结束。
第二种情况是,这个记录要更新的目标页不在内存中。这时,InnoDB 的处理流程如下:
对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;
对于普通索引来说,则是将更新记录在 change buffe,语句执行就结束了。
change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。
2.2 change buffer 的使用场景
change buffer 只限于用在普通索引的场景下,而不适用于唯一索引。
注意:因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。反过来,假设一个业务的更新模式是写入之后马上会做查询,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样change buffer 反而起到了副作用。
redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。
04、总结
在选择索引和编写利用这些索引的查询时,有如下三个原则始终需要记住:
1.单访问时很慢的。特别是在机械键盘存储中。如果服务器从存储中读取一个数据块只是为了获取其中一行,那么久浪费了很多工作。最好读取的块中能包含尽可能多所需要的行。使用索引可以创建位置引用以提升效率。
2.按顺序访问范围数据是很快的,这有两个原因。第一,顺序I/O不需要多行磁盘寻道,所以比随机I/O要快很多。第二是,如果服务器能够按需要顺序读取数据,那么就不再需要额外的排序操作,并且Group By 查询也无须再做排序和将行按组进行聚合计算了。
3.索引覆盖查询是快的。如果一个索引包含了查询需要的所有列,那么存储引擎就不需要再回表查找行。这避免了大量的单行访问。