MySQL中一张表可以支持多个索引。使用哪个索引是由MySQL来确定的。
explain + sql;查看语句的执行情况。
set long_query_time=0;设置慢查询阈值,单位秒。
set slow_query_log=1;开启慢查询日志,0表示关闭。
slow_query_log_file;慢查询日志存放路径。
优化器的逻辑
选择索引是优化器的工作。
优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。(扫描行数、临时表、排序等影响)
扫描行数
MySQL在真正开始执行语句之前,并不能精确的知道满足这个条件的记录有多少条,只能根据统计信息来估算记录数。
这个统计信息就是索引的“区分度”。一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,称之为“基数”(cardinality)。这个基数越大,索引的区分度越好。
show index from t;可以查看一个索引的基数。并不是准确的数字。
MySQL索引的基数通过采样统计的方法得到。
InnoDB默认会选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,得到这个索引的基数。
数据表会持续更新,索引统计信息也不会固定不变,当变更的数据行数超过1/M的时候,会自动触发重新做一次索引统计。
统计信息不会每次更新都重新统计。
在MySQL中,有两种存储索引统计的方式,可以通过innodb_stats_persistent的值来选择:
- 设置为on的时候,表示统计信息会持久化存储。默认的N是20,M是10.
- 设置为off的时候,表示统计信息只存储在内存中。默认的N是8,M是16。
索引统计只是一个输入,对于一个具体的语句,优化器还要判断,执行这个语句本身要扫描多少行。
explain SQL;其中rows字段表示的是预计扫描行数。真正需要扫描多少行,得执行了才知道。
使用普通索引,每次从索引上拿到一个值,都要回到主键索引上查出整行数据,这个回表的代价优化器也计算。而如果扫描全部的行,是直接在主键索引上扫描的,没有额外的代价。
索引选择异常处理
join、group、order、limit等操作都是在server层执行器中进行。执行器调用存储层的接口只能获取最原始的数据。然后一条一条的进行过滤,然后再调引擎层获取下一条。
1属于索引统计信息不准确
234属于优化器误判
1.analyze table
analyze table t;– 执行过程中会加MDL读锁。
修正统计信息,重新统计索引信息。
实践中,explain的结果预估的rows值跟实际情况差距比较大,可以采用这个方法处理。
sessionA | sessionB |
---|---|
start transaction with consistent snapshot | |
delete from t; call idata();– 存储过程,insert十万条数据 | |
explain select * from t where a between 10000 and 20000; | |
commit |
1.为什么没有session A,session B扫描的行数是1W
由于mysql是使用标记删除来删除记录的,并不从索引和数据文件中真正的删除(加索引慢,删索引秒删)。
如果delete和insert中间的间隔相对较小,merge线程还没有来得及清理该记录。
如果主键相同的情况下,新插入的insert会沿用之前删除的delete的记录的空间。(看上去是覆盖了原来的10万行)。
由于相同的数据量以及表大小,所以导致了统计信息没有变化。
2.为什么开启了session A,session B扫描行数变成3W
由于session A开启了一致性读,目的为了保证session A的可重复读,insert只能另起炉灶,不能占用delete的空间。所以出现的情况就是delete虽然删除了,但是未释放空间,insert又增加了空间。导致统计信息有误。
为了保证sessionA的可重复读(未提交),数据页并没有实际删除,undo log也还没有回收,新的insert又产生了新的数据页,之前的数据每一行数据都有两个版本,旧版本是delete之前的数据,新版本是标记为delete的数据,这样索引a上的数据其实就有两份。索引的统计选择了N个数据页进行采样,这部分数据页不受到前台事务的影响,索引整体统计值会变大,直接影响了索引选择的准确性。
2.force index
select * from t force index(a) where …;
采用force index强行选择一个索引。起到“矫正”的作用。
MySQL会根据词法分析的结果分析出可能可以使用的索引作为候选项,然后在候选索引列表中依次判断每个索引需要扫描多少行。如果force index指定的索引在候选索引列表中,就直接选择这个索引,不再评估其他索引的执行代价。(不触发索引重新统计,不会改变explain 中预估扫描行数的值)
3.修改sql语句
1 | select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1; |
优化器放弃使用索引a,说明a还不够合适。
优化器选择使用索引b,是因为它认为使用索引b可以避免排序(b本身是索引,已经是有序的了,如果选择索引b的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。
但是变更不及时。
如果使用索引a进行查询,就扫描索引a的1000个值,然后取到对应的主键id,再到主键索引上去查出每一行,然后根据字段b来过滤。
如果索引索引b进行查询,就扫描索引b的50001个值,取到对应的主键id,依次回到主键索引上取值再判断。一条过滤完再获取并处理下一条数据。
没有order by则默认按照“查询使用的索引”排序。
1 | select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b,a limit 1; |
order by b,a;要求按照b,a排序,就意味着使用这两个索引都需要排序。扫描行数成了影响决策的主要条件。
但是修改了语句语义。
1 | select * from (select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1; |
用limit 100 让优化器意识到,使用b索引的代价很高。??优化器计算代价的细节待补充。
不具备通用性。
4.新建一个更合适的索引|删掉误用的索引
经过索引优化过的库,再碰到这种bug,找到一个更合适的索引一般比较难。
优化器错误选择的索引若没有必要存在,就删掉这个索引。