MySQL实战之——索引选择分析

MySQL实战之——索引选择分析


再MySQL中,一张表是可以有多个索引的,那么当我们没有指定使用哪个索引的时候,MySQL在执行SQL语句的时候, 索引的选择发生在优化器中 ,那么本片博客带你来分析一种情况: MySQL选择索引出错

1. 建表插数据过程(实验准备)

先来建立一张表:

mysql> create table test3(
    -> id int(11) primary key,    #主键索引
    -> a int(11),
    -> b int(11),
    -> index(a),     #建立普通索引
    -> index(b));
Query OK, 0 rows affected (0.93 sec)

建立好test3这张表,下面查看一下它的结构:

mysql> desc test3;
+-------+---------+------+-----+---------+-------+
| Field | Type    | Null | Key | Default | Extra |
+-------+---------+------+-----+---------+-------+
| id    | int(11) | NO   | PRI | NULL    |       |
| a     | int(11) | YES  | MUL | NULL    |       |
| b     | int(11) | YES  | MUL | NULL    |       |
+-------+---------+------+-----+---------+-------+
3 rows in set (0.19 sec)

接下来我们向这张表中插入10万行记录,取值按整数递增,即:(1,1,1)、(2,2,2)…(100000,100000,100000);这里采用存储过程来进行快速插入:

mysql> delimiter ;;
mysql> create procedure idata()
    -> begin
    -> declare i int;
    -> set i=1;
    -> while(i<=100000)do
    -> insert into test3 values(i,i,i);
    -> set i=i+1;
    -> end while;
    -> end;;
Query OK, 0 rows affected (0.20 sec)

mysql> delimiter ;
mysql> call idata();

在进行插入过程前,注意先设置下面两个参数为0,否则插入过程时间将会高达10分钟左右,下面参数关闭了
设置这两个参数后,会发现10万条记录的插入只需要几秒;

mysql> set global innodb_flush_log_at_trx_commit = 0;
Query OK, 0 rows affected (0.00 sec)
mysql> set global sync_binlog = 0;
Query OK, 0 rows affected (0.04 sec)

2. 数据的查询

1. 正常案例

基于上面建立的表,执行下面的查询语句分析(用explain):

mysql> explain select * from test3 where a between 10000 and 20000;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows  | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
|  1 | SIMPLE      | test3 | NULL       | range | a             | a    | 5       | NULL | 10001 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
1 row in set, 1 warning (0.15 sec)

可以看到,key的类型是a,这符合我们的预期,执行该语句使用了a索引,索引很快就能执行完成;

3. 异常案例一

1. 案例背景

接下来我们做如下操作,执行两个事物分别如下:
事物A:
在这里插入图片描述

2. 异常案例分析(对于异常案例一的分析)

1. 优化器如何选择索引

导致上面索引选错的原因就是优化器选择索引的时候出现了问题,那么为什么会选错,此前我们必须先了解优化器选择索引的标准是什么;
我们知道:优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行SQL语句;

  • 在数据库中,扫描行数是影响执行代价的因素之一
    • 扫描行数越小,则执行代价越小,因为访问磁盘的次数就会越少,消耗的CPU资源越少;
  • 注意:执行代价的影响因素不止上面这一个,比如还有是否使用临时表、是否排序等等因素;

那么,扫描行数是如何判断的呢?

在上面的查询语句中,其实MySQL并不能知道满足这个查询的行数有多少,只能根据统计信息来估算记录数;

而这个统计信息就是索引的区分度:

  • 一个索引上不同的值越多,这个索引的区分度就越好,而一个索引上不同值的个数,我们称之为基数
    • 所以基数越大,这个索引的区分度就越好
  • 我们可以通过show index from test3;这条语句来查看这个表中索引信息;
    • Cardinality 这一列就代表该索引的基数;

进一步,那么MySQL是如何得到索引的基数的呢?

  • 采用了采样统计的方法;
    • 由于得到精确结果需要取整张表,代价太高,所以只能选择”采样统计“;
  • 采样统计方法过程:
    • InnoDB默认选择N个数据页,统计这些页面上的不同值,算出平均值,最后将平均值*这个索引的页面数 就得到了这个索引的基础;

我们可以通过explain SQL语句来查看执行这条SQL语句的具体信息,里面有一列为rows,这个数就是优化器预估执行这条语句预计扫描行数;
(img-3knz3fG8-1570930835822)(C:\Users\Laptop\AppData\Local\Temp\1570787975512.png)]

从这张图可以看到,在上面的row还算差不多,而下面的,本来应该在10000左右,结果为37116,差距就有点大了,看来选错索引的原因就在这了;
可能有人会问,上面不走索引下预算扫描104620行,下面预算只需要扫描37116行,那么为什么优化器还选择上面呢?

  • 原因:在下面使用a索引查询时,需要回表查询,即回到主键取出整行数据,这个代价优化器也算入的,而在主键上,虽然扫描行数多,但不需要回表;
  • 从结果来看,优化器认为不使用索引的代价比较小,这是正确的(假如下面的预估行数是正确的);
    但从我们查询的结果来看,不使用索引时间比较长,所以我们认为优化器的选择有误;

那么归于一切,还是因为估算行数差距太大导致优化器综合来看选择了不使用索引,即假如估算使用索引a要查询的行数正确的话,后面就肯定会选择a索引,因为在最开始的正确案例中预估行数为10001是正确的,优化器就选择了索引a去查询;

2. 如何修正统计信息(以及异常案例一总结)

对于异常案例一,我们只需修复统计信息即可,可以用如下的命令:(analyze

mysql> analyze table test3;
+-------------+---------+----------+----------+
| Table       | Op      | Msg_type | Msg_text |
+-------------+---------+----------+----------+
| java7.test3 | analyze | status   | OK       |
+-------------+---------+----------+----------+
1 row in set (0.15 sec)

用上面的语法后,再在异常案例一中执行那句查找就会发现预估行数就正确了(10001),此时优化器就选择了索引a来进行查询;

总结:

  • 对于案例一,在我的实践过程中其实并没有出现索引失效的情况,估计是出现上面那种情况是小概率的,但我们要预防那种情况的出现,能找出应对策略(修正统计信息);
  • 所以在实践中,如果发现MySQL的预估行数(explain语句后的rows那一行)与实际要查询的行数相差太大导致的优化器选择索引出错,则可以通过修正统计信息来进行更正; (⭐⭐)

4. 异常案例二

1. 案例背景

对于异常案例一,我们知道了,优化器索引失效的原因是统计信息的偶尔出错(预估行数差距较大),对于这种情况,通过analyze就可以解决大多数情况,但是我们得注意,优化器不仅仅只看扫描行数;

下面来看看这条查询语句(还是我们那个表)

mysql> select * from test3 where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
Empty set (0.17 sec)

可以知道,我们查询的结果就是为空;
order by 表示排序查询,这里默认升序,配合limit表示结果取第一个,如果后面是x,则代表取前x个
例如下面的查询:

mysql> select * from test3 where (a between 1 and 1000) and (b between 500 and 1000) order by b limit 5;
+-----+------+------+
| id  | a    | b    |
+-----+------+------+
| 500 |  500 |  500 |
| 501 |  501 |  501 |
| 502 |  502 |  502 |
| 503 |  503 |  503 |
| 504 |  504 |  504 |
+-----+------+------+
5 rows in set (0.00 sec)

2. 案例分析

对于上面那个查询,我们试着来分析一下优化器会选择哪个索引来执行那条SQL语句:(只有可能下面两种方案

  • 如果使用索引 a 进行查询,那么就是扫描索引 a 的前 1000 个值,然后取到对应的 id,再到主键索引上去查出每一行,然后根据字段 b 来过滤。显然这样需要扫描 1000 行。
  • 如果使用索引 b 进行查询,那么就是扫描索引 b 的最后 50001 个值,与上面的执行过程相同,也是需要回到主键索引上取值再判断,所以需要扫描 50001 行。

按照我们对异常案例一的分析,查询行数是优化器选择索引的一个因素,这里显然优化器会选择索引a,因为它的查询行数只有1000行,而索引b需要查询50001行,那我们来explain一下,看优化器到底选择了哪个索引;

mysql> explain select * from test3 where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows  | filtered | Extra                              |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
|  1 | SIMPLE      | test3 | NULL       | range | a,b           | b    | 5       | NULL | 50128 |     1.00 | Using index condition; Using where |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
1 row in set, 1 warning (0.00 sec)

太惊讶了,优化器居然选择了索引b,为什么呢?看到这个结果,我们还不确定索引到底选择正确没,下面我来看看这两种方案分别需要的查询时间:

#这种优化器选择了索引b,上面已经测试了,时间为0.06秒
mysql>  select * from test3 where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
Empty set (0.06 sec)
#下面这种我们强制使用索引a,force语句,执行时间为0秒
mysql>  select * from test3 force index(a) where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
Empty set (0.00 sec)

可以看到使用索引a的时间很明显比使用b的时间短,说明优化器在选择索引的时候又选择错了,这是为什么呢?

  • 这里我们可以排除统计信息出错的问题,可以看到上面中rows为50128,与实际相差不大(当使用explain 查看强制使用a索引,rows为1000,完全正确),说明优化器知道选择索引b要查询这么多行还偏要选择它;

⭐原因:因为我们使用了order by语句对结果进行排序,而索引b本身就是有序的,如果选择它作为查询索引就不用排序了,只需要遍历,此时就不再考虑扫描行数的影响,即使扫描行数更多,也判定为代价更小;

3. 解决方案

1. force 语句

上面我们已经提出了第一种方案:使用force 语句强制选择我们想要的索引去执行SQL语句;

但他的缺点比较多:

  • 写法上不优美;
  • 如果索引更改了名字,那里的语句也得更改索引名字;
2. 修改语句

在上面的案例中,我们将原查询语句中的order by b limit 1改成 order by b,a limit 1;
这两句逻辑上是一样的,并不会影响结果,来看看结果:

mysql> explain select * from test3 where (a between 1 and 1000) and (b between 50000 and 100000) order by b,a limit 1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                              |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | test3 | NULL       | range | a,b           | a    | 5       | NULL | 1000 |    50.00 | Using index condition; Using where; Using filesort |
+----+-------------+-------+------------+-------+---------------+------+---------+------+------+----------+----------------------------------------------------+
1 row in set, 1 warning (0.00 sec)

可以看到这里优化器选择了索引a,选择正确了,这是因为此时需要根据a,b都要排序,所以选择查询行数较少的一个呗!

但这个方法也不是通用的优化手段,只是这条语句刚好允许这么修改后结果上也一样,但有的语句就不能随意更改了;

3. 其他方法

还有其他的办法就是可以:

  • 新增更合适的索引
  • 删掉优化器误用的索引;
    • 对于上面的例子,索引b其实就可以直接删除,也没有其他的利用价值了,反而会导致优化器去用它;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值