5 索引实战

3 普通索引和唯一索引的选择

在这里插入图片描述
以如图的数据,从这两种索引对查询语句和更新语句的性能影响来进行分析

3.1 查询过程

假设执行查询的语句是:

select id from T where k=5

这个查询语句在索引树上查找的过程:先是通过B+树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过⼆分法来定位记录。

对于普通索引来说,查找到满足条件的第⼀个记录(5,500)后,需要查找下⼀个记录,直到碰到第⼀个不满足k=5条件的记录。

对于唯⼀索引来说,由于索引定义了唯⼀性,查找到第⼀个满足条件的记录后,就会停止继续检索。

性能差距其实很小:

InnoDB的数据是按数据页为单位来读写的;
也就是说,当需要读⼀条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读⼊内存。

在InnoDB中,每个数据页的大小默认是16KB。

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

当然,如果k=5这个记录刚好是这个数据页的最后⼀个记录,那么要取下⼀个记录,必须读取下⼀个数据页,这个操作会稍微复杂⼀些;
但是,比如对于整型字段,⼀个数据页可以放近千个key,因此出现这种情况的概率会很低;
所以计算平均性能差异时,仍可以认为这个操作成本对于现在的CPU来说可以忽略不计。

3.2 更新过程

change buffer:

当需要更新⼀个数据页时,如果数据页在内存中就直接更新;
而如果这个数据页还没有在内存中的话,在不影响数据⼀致性的前提下,InooDB会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入这个数据页了。

在下次查询需要访问这个数据页的时候,将数据页读⼊内存,然后执行change buffer中与这个页有关的操作,通过这种方式就能保证这个数据逻辑的正确性。

change buffer是可持久化的

将change buffer中的操作应用到原数据页,得到最新结果的过程称为merge。除了访问这个数据页会触发merge外,系统有后台线程会定期merge。在数据库正常关闭(shutdown)的过程中,也会执⾏merge操作。

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

什么条件下可以使用change buffer?

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

因此也只有普通索引可以使用

了解了change buffer之后,开始看流程的区别:

  • 如果要在这张表中插⼊⼀个新记录(4,400)的话:

1 这个记录要更新的目标页在内存中:

对于唯⼀索引来说,找到3和5之间的位置,判断到没有冲突,插⼊这个值,语句执行结束;

对于普通索引来说,找到3和5之间的位置,插⼊这个值,语句执行结束。

差别只是⼀个判断,只会耗费微小的CPU时间。

2 这个记录要更新的目标页不在内存中:

对于唯⼀索引来说,需要将数据页读⼊内存,判断有没有冲突,没有的话插入这个值,语句执行结束;

对于普通索引来说,则是将更新记录在change buffer,语句执行就结束了。

将数据从磁盘读⼊内存涉及随机IO的访问,是数据库里成本最高的操作之⼀;
change buffer因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

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

3.3 change buffer和redolog的区别

  • 写请求处理

执行语句如下:

insert into t(id,k) values(id1,k1),(id2,k2);

查找到位置后,k1所在的数据页在内存(InnoDB buffer pool)中,k2所在的数据页不在内存中;
如图所示是带change buffer的更新状态
在这里插入图片描述
涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。

系统表空间就是用来放系统信息的,比如数据字典什么的,对应的磁盘文件是ibdata1;
数据表空间就是⼀个个的表数据文件,对应的磁盘文件就是 表名.ibd

  1. Page 1在内存中,直接更新内存;
  2. Page 2没有在内存中,就在内存的change buffer区域,记录下“我要往Page 2插⼊⼀行”这个信息,就不用读磁盘了
  3. 将上述两个动作记入redo log中(图中3和4)。

做完这三步,事务就可以完成了,全部写了两处内存,一处磁盘(两次合在一起写了一次),而且还是顺序写的,执行这条更新语句的成本很低;
同时图中的两个虚线箭头,是后台操作,不影响更新的响应时间。

  • 读请求处理

执行语句如下:

select * from t where k in (k1, k2)

如果读语句发生在更新语句后不久,内存中的数据都还在,那么此时的这两个读操作就与系统表空间(ibdata1)和 redolog(ib_log_fileX)无关了。所以在图中就没画出这两部分:
在这里插入图片描述

  1. 读Page 1的时候,直接从内存返回。(可以看到,WAL想读数据不一定要读盘,这里虽然磁盘上还是
    之前的数据,但是这里直接从内存返回结果,结果是正确的。)
  2. 要读Page 2的时候,需要把Page 2从磁盘读⼊内存中,然后应⽤change buffer里面的操作⽇志,生成⼀个正确的版本并返回结果。
  • 总结

redo log 主要节省的是随机写磁盘的IO消耗(转成顺序写)

change buffer主要节省的则是随机读磁盘的IO消耗。(不用读磁盘就能写)

如果业务可以接受,从性能⻆度出发建议优先考虑非唯⼀索引,这样可以使用change buffer机制

4 选错索引

不断地删除历史数据和新增数据时,MySQL会选错索引

优化器选择索引的标准:扫描的行数、是否使用临时表、是否排序等

使用采样统计得到索引的基数

如果使用索引a,每次从索引a上拿到⼀个值,都要回到主键索引上查出整行数据,这个代价优化器也要算进去
的;
而如果是直接在主键索引上扫描的,则没有额外的代价。

如果发现索引错了,原因只是索引统计不准确的话,可以使用:

analyze table 表

可以统计索引信息

其他情况的话,可以使用force index强行选择一个索引,这第一种方法不好,一般都是线上出问题了再修改,如果改sql语句,那么还有测试和重新发布,不敏捷,所以可以使用第二种方法,考虑修改语句,引导MySQL使用期望的索引:
比如有a,b索引,sql语句为select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;a索引比b表现得好,但MySQL却选择了b,此时可以order by b limit 1改为order by b,a limit 1,语义逻辑相同,索引选择就成了a了;
这种方法不通用,第三种方法则为limit 100,让优化器意识到,使用b索引代价是很⾼的,此方法也不具备通用性;
种方法是,在有些场景下,可以新建⼀个更合适的索引,来提供给优化器做选择,或删掉误用的索引,这种比较通用

5 给字符串字段加索引

可以使用整个字符串作为索引:
在这里插入图片描述
也可以定义字符串的一部分作为索引(因为MySQL支持前缀索引):
在这里插入图片描述
可以看到,使用前缀索引的话,占用的空间会更小,但是可能会增加额外的记录扫描次数,因为前缀可能重复

因此原则是:可以使用前缀索引,只要定义好长度(关注区分度),就可以做到既节省空间,又不用额外增加太多的查询成本。

方法:首先使用下面这个语句,算出这个列上有多少个不同的值:

select count(distinct 字符串字段) as L from 表名;

然后,依次选取不同长度的前缀来看这个值,比如要看⼀下4~7个字节的前缀索引,可以用这个语句:

select
count(distinct left(字符串字段,4))as L4,
count(distinct left(字符串字段,5))as L5,
count(distinct left(字符串字段,6))as L6,
count(distinct left(字符串字段,7))as L7,
from 表名;

需要预先设定⼀个可以接受的损失比例,比如5%;
然后,在返回的L4~L7中,找出不小于 L * 95%的值,假设这里L6、L7都满足,就可以选择前缀⻓度为6。

使用前缀索引除了可能会增加扫描行数影响到性能外,还有就是某些语句使用全字符串字段做索引可能可以利用覆盖索引,如select id,字符串字段 from 表名 where 字符串字段='[email protected]';,利用覆盖索引查到结果后就直接返回了,不用回表,前缀索引则每匹配一个都要去回表查询,这个是需要考虑的第二个点

  • 其他方式

1 倒序存储:如身份证号,后6位没有重复逻辑,存储身份证号的时候把它倒过来存,查询的时候使用reverse倒序即可

select field_list from t where id_card = reverse('input_id_card_string');

2 hash字段:如身份证号,可以在表上再创建⼀个整数字段,来保存身份证的校验码,同时在这个字段上创建索引;
然后每次插⼊新记录的时候,都同时用crc32()这个函数得到校验码填到这个新字段;
由于校验码可能存在冲突,也就是说两个不同的身份证号通过crc32()函数得到的结果可能是相同的,所以查询语句where部分要判断id_card的值是否精确相同;
这样,索引的长度变成了4个字节,比原来小了很多。

alter table t add id_card_crc int unsigned, add index(id_card_crc);
select field_list from t where id_card_crc=crc32('input_id_card_string') and id_card='input_id_card_string

使用倒序存储和使用hash字段这两种方法的异同点:

相同点:都不支持范围查询。

倒序存储的字段上创建的索引是按照倒序字符串的方式排序的,已经没有办法利用索引方式查出身份证号码在[ID_X, ID_Y]的所有市民了。同样地,hash字段的方式也只能支持等值查询。

不同点:主要体现在以下三个方面:

  1. 从占用的额外空间来看:倒序存储方式在主键索引上,不会消耗额外的存储空间,⽽hash字段方法需要增加⼀个字段;
    当然,倒序存储方式使用4个字节的前缀长度应该是不够的,如果再长⼀点,这个消耗跟额外这个hash字段也差不多抵消了。
  2. 在CPU消耗方面:倒序方式每次写和读的时候,都需要额外调用⼀次reverse函数,而hash字段的⽅式需要额外调用⼀次crc32()函数;
    如果只从这两个函数的计算复杂度来看的话,reverse函数额外消耗的CPU资源会更小些。
  3. 从查询效率上看:使用hash字段方式的查询性能相对更稳定⼀些;
    因为crc32算出来的值虽然有冲突的概率,但是概率非常小,可以认为每次查询的平均扫描行数接近1。而倒序存储⽅式毕竟还是用的前缀索引的方式,也就是说还是会增加扫描行数。
  • 总结

字符串字段创建索引可以使用的方式:

  1. 直接创建完整索引,这样可能比较占用空间;
  2. 创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引;
  3. 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题;
  4. 创建hash字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式⼀样,都不支持范围扫描。
发布了235 篇原创文章 · 获赞 264 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/103531167