背景
最近我负责的服务被公司安全组件扫出了全表扫风险。表信息大概如下:CREATE TABLE task_operate_record
(
task_no varchar(32),
content varchar(2048)
);
CREATE INDEX uidx_task_no ON task_operate_record (task_no)
触发全表扫sqlUPDATE TABLE task_operate_record SET content = 'xxx'
WHERE task_no = 'XXXX';
问题分析
首先,从表结构可知,记录表中包含了一个唯一索引。并且有问题的SQL中,唯一索引task_no也在查询条件里。
明明有索引但实际上没有生效,非常奇怪。
执行explain分析
|
发现MYSQL执行计划中确实可以识别到task_no索引(possible_key=task_no),但是真正执行的时候却使用了主键索引。
说明:SQL执行引擎发现了索引,但是执行时,引擎对SQL以及使用的索引进行了优化。让SQL直接走主键索引。
之后,我把UPDATE改为SELECT,再次执行EXPLAIN。结果是SQL执行引擎不但发现了索引,并且在实际查询中也确实用到了索引。
BUG WHY???
我们来分析下索引的本质。之前我的文章中也分析过MYSQL索引。总结来说MYSQL索引的数据结构是B+树,共分两类,聚集索引和非聚集索引。
所谓聚集索引就是主键索引,是B+树中除了保存idx也会保存data。非聚集索引只记录索引,它的叶子节点保存的是主键ID。
那么上面两个CASE具体的执行过程:
对于查询,MYSQL会先去二级索引的B+树上查询到符合条件的主键ID。然后拿着ID去主键索引的B+树上查询加载数据。
对于更新(命中索引),过程跟上面差不太多。唯一的区别是需要在定位到数据的时候同时更新查询到的数据。就是说相交查询,这里多了一步更新操作。
分析到这里,貌似也不能解决上面的问题。
这条路不通,于是我去数据库查了下数据量。由于这个全表扫SQL是线下库爆出来的风险。所以可想而知,线下库本来就是联调测试用的,总共不到100条数据。
我们再回头分析下UPDATE,如果命中索引,它的自行过程比查询多个一步更新。也要查两棵B+树。
但是如果没命中索引呢?
引擎直接走主键索引全表扫,随后更新数据。
到这里大家应该看出问题了吧,^_^
结论
在数据量比较小的时候,在执行更新操作时。即使查询条件存在二级索引,MYSQL执行引擎也可能不使用二级索引。因为在数据量比较小的场景下,与查询两次B+树随后更新比起来,直接全量查一次B+树,再更新数据代价可能更小。
验证
按如下方式调整更新SQLUPDATE task_operate_record FORCE INDEX (`uidx_task_no`) SET content = 'xxx' WHERE task_no = 'xxx';
分别执行调整前后的SQL,发现走索引的更新比不走索引的耗时更多(索引两者耗时都比较小)。