2. mysql 为什么会选错索引

上节我们讲了mysql的整体架构,知道了优化器的作用是优化sql,选择索引,生成执行计划。索引是优化器阶段自己选择的,优化器大部分情况下索引的选择都是比较合理的,但是也有特殊情况下,优化器选择的索引并不是最优的。下面我们通过实验来说明这个问题,并给出优化方案(需要说明的是本实验的环境是mysql版本是5.7,事务隔离级别是RR,事务自动提交)。

首先我们来建一个表,表里面有id、a、b三个字段,下面是建表语句

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`),
  KEY `b` (`b`)
) ENGINE=InnoDB;

然后我们向表t里面插入10万行记录,记录是递增的 (1,1,1), (2,2,2), (3,3,3)......(100000,100000,100000),插入语句如下:


delimiter ;;
create procedure idata()
begin
  declare i int;
  set i=1;
  while(i<=100000)do
    insert into t(a,b) values(i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();

这里要注意的是我的mysql默认是自动提交,且sync_binlog=1,innodb_flush_log_at_trx_commit=1,所以说上面的存储过程每次插入数据,在提交的时候binlog和redolog都会写到磁盘,这大大增加了IO交互,导致整个插入过程非常慢,在我的电脑上运行了30分钟才插入完成,解决方法有很多可以设置sync_binlog=0,innodb_flush_log_at_trx_commit=0sync_binlog=0表示每次提交只write(从binlog cache 写到page cache)不fsync(写到磁盘),innodb_flush_log_at_trx_commit=0表示每次提交只写到redo log buffer(就是不做任何操作,因为redo log最开始生成的时候就在redo log buffer 中,Innodb 后台有一个线程,每1s就会将redo log buffer里面的数据调用write写到page cache 然后再调用 fsync 写到磁盘),还需注意的是binlog cache 是线程私有的,redo log buffer是公共的,所以处以prepare阶段的redo log是可能被持久化的。

插入数据后,我们分析一下sql 语句的执行计划

EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;

你肯定会想这还不简单吗?字段a上有索引肯定走的索引a,事实也是这样的,下面是执行计划的截图


正常索引.png

我们再做如下操作,看看情况怎样

sessionA sessionB
start transaction with consistent snapshot;
delete from t;
call idata();
EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;
commit;

start transaction with consistent snapshot; 代表的意思是执行这句话就启动了事务,就生成一致性视图,如果只是start transaction 只有在第一次查询的时候才会生成一致性视图,需要说明的是start transaction并不是事务的起点,在执行到start transaction之后的第一个操作才启动事务,第一个查询语句生成的trx_id 是虚假的,是为显示用的,只有事务性操作生成的trx_id 才会存于一致性视图数组中。这个一致性视图数组是mvcc实现的基础。

然后,我们现在再来看一下sessionB 的 EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;

有误的索引.png

发现此时是全表扫描,key处为空,rows为100015行,没有走索引a,和我们预想的可能不太一样,为了更准确的说明,是mysql选错了索引,我们再来做个对比。

set GLOBAL slow_query_log = 1;

set long_query_time = 0;

SELECT * from t where a BETWEEN 10000 and 20000;/**Q1**/

SELECT * from t FORCE INDEX(a) where a BETWEEN 10000 and 20000;/**Q2**/
  • 第一条语句是打开慢日志的开关 ,这是个全局的开关
  • 第二条语句是慢日志的标准,就是说什么样的语句是慢日志,才会被慢日志记录下来。这里的意思是只要操作时间超过0s就会记录下来,所以这里是记录所有的操作语句(增删改查都会记录)。
  • 第三条是在sessionB下的查询语句(Q1)
  • 第四条是在sessionB下的查询语句(Q2),但是强制使用的索引a

我们来看看Q1与Q2在慢日志中的具体查询信息

# Time: 2021-05-03T02:17:03.047811Z
# User@Host: root[root] @ localhost [::1]  Id:    20
# Query_time: 0.035942  Lock_time: 0.000000 Rows_sent: 10001  Rows_examined: 100000
SET timestamp=1620008223;
SELECT * from t where a BETWEEN 10000 and 20000;

# Time: 2021-05-03T02:18:08.055825Z
# User@Host: root[root] @ localhost [::1]  Id:    20
# Query_time: 0.021942  Lock_time: 0.000000 Rows_sent: 10001  Rows_examined: 10001
SET timestamp=1620008288;
SELECT * from t FORCE INDEX(a) where a BETWEEN 10000 and 20000;

可以看到没有使用强制索引a的Q1语句确实是全表扫描了Rows_examined: 100000,Query_time: 0.035942,强制使用了索引a的Q2语句Rows_examined: 10001,Query_time: 0.021942,到这里我们可以肯定说,mysql没有使用最优的方案来查询,下面我们就来具体分析分析为什么!

首先我们要明白的是优化器是怎么选择索引的,索引的选择有很多因素影响包括是否排序、是否使用到临时表、扫描行数等因素,优化器是综合考虑各个因素选择的最优方案,这里我们的语句SELECT * from t where a BETWEEN 10000 and 20000;,没有使用到临时表,也没有排序,所以这里最关键的影响就是扫描行数,扫描行数越少,访问磁盘的次数越少,需要消耗的CPU资源就越少。接下来问题又来了,扫描行数是怎么判断的呢?

我们在优化器阶段就判断了扫描的行数(执行计划中的Rows),但这个扫描的行数是预估的,不是准确的,为什么要预估呢?因为扫描所有数据需要消耗大量的资源,比如CPU等等,那是怎么预估的呢。这里有个基数的概念,基数是什么呢?基数是一个索引上不同值的个数,索引的基数越大说明索引的区分度越好,扫描的行数可能就越少,索引的基数不是实时变化的,它也是一个统计的结果,那它是怎么统计的呢?

show index from t 来查看表t上所有索引的基数

基数的统计 :基数的统计是采用统计的方法计算的,在统计的时候默认随机取N页数据,统计这N页数据所有不同的数值的个数得到平均值,然后再乘以这个索引的总页数,最后得到基数。数据表里面的数据在不断变更,当变更的数据超过总行数的1/M时会再次触发统计,修正原先统计的基数。至于M和N 到底是多少?在mysql中有两种存储索引的方式,可以通过设置参数innodb_stats_persistent 的值来选择:

  • 设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。

  • 设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16

通过基数,优化器可以判断会不会选择该索引(这里猜测有一个阈值),如果区分度实在是太小则放弃该索引(这就是建议我们不要在区分度不高的列上加索引,因为没啥用),经过第一层过滤,优化器还要判断扫描行数,就是执行计划的rows,这个rows也是一个统计的结果(根据where条件在索引上统计),看rows是不是太大,如果太大,优化器想我还不如直接全表扫描算了,免得还要回表,所以这里还有一个扫描行数的权衡,到这里我们基本讲完了索引是怎么选择的了。

回到本文的示例,sessionB中的查询语句为什么不走索引a呢?那就是mysql的统计的rows太大,优化器觉得使用索引a不划算 。那为什么mysql的统计的rows太大,是因为 sessionA语句来了就start transaction with consistent snapshot; ,这个语句一执行就创建了视图,根据mvcc规则,它要看见它能看见的数据,sessionB执行删除操作,只是把每行的数据标记为已删除,实际并没有删除(ibd文件大小并没有发生改变),新插入的数据因为id是递增的不能复用被标记删除的位置,所以通过页分裂的形式把新增的数据放入对应的数据页中,这样在统计rows的时候因为连带被标记删除的行一起统计,导致rows统计的行数偏大,从而进一步导致优化器选择了全表扫描。

这里要补充一下页的知识:

  1. Innodb与磁盘交互的最小单位是页,所以说加载数据到内存是按照页来加载的而不是一行一行加载,索引只能定位到页不能定位到具体哪一行,哪一行只能在页中通过二分法来进行查找

  2. 页与页之间是通过双向链表来链接的,页内部的数据行是通过单向链表来链接的。

  3. 插入数据可能会导致页的分裂,尤其是非自增主键,从而导致页空洞(就是页的利用率不高);删除数据会导致数据的空洞,也可能会导致页的合并

  4. 页的结构 如附件表所示

那我们知道了原因,如何进行优化呢。主要有以下几种解决方法

  1. analyze table ,通过该命令重新统计基数信息
    analyze.png
  1. 通过 force index ,强制使用某个索引,告诉优化器你不用选择索引了,我就用这个,但是该方案十分的不优雅

    MySQL 会根据词法解析的结果分析出可能可以使用的索引作为候选项,我们经常可以看到执行计划的possible_key有几个候选项,然后在候选列表中依次判断每个索引需要扫描多少行。基本上是选择扫描行数最少的作为最后的索引,如果 force index 指定的索引在候选索引列表中,就直接选择这个索引,不再评估其他索引的执行代价。

  2. 在不改变语义的情况下,修改sql语句,引导执行计划选择我们想要的索引

  3. 新建一个更合适的索引或者删除误用或没有的索引

小结

本篇文章我们具体分析了mysql是怎么选择索引的,以及为什么会选错索引,并给出了四种优化方案,谢谢。

附件

页结构表:


页结构.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容