mysql为什么有时会选错索引
-
场景例子:一张表里有a,b两个字段,并分别建立以下索引
CREATE TABLE `t` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `a` (`a`), KEY `b` (`b`) ) ENGINE=InnoDB;
-
表中数据从(1,1,1)到(100000,100000,100000)共10万行记录。
-
接下来分析一条sql语句:select * from t where a between 10000 and 20000;
-
做如下操作:
-
我们可以设置慢日志阈值为0,表示这个线程接下来的语句都会被记入慢查询日志中;set long_query_time=0;可以看到执行B时,走了全表扫描,没有走索引。
-
-
为什么没有按指定的a索引扫描呢?
- 而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。
当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。我们这个简单的查询语句并没有涉及到临时表和排序,所以MySQL选错索引肯定是在判断扫描行数的时候出问题了。 - 基数:
- mysql进行扫描前,并不知道满足这个条件的记录有多少条,而只能根据统计信息来估算记录数。即索引区分度,一个索引上不同的值越多,这个区分度越好,而一个索引上不同的值的个数称为基数,基数越大,索引区分度越好。可以使用show index from 表名,查看结果。
- 优化器会估算回表的代价,同时评估主键索引扫描的没有额外代价,选择执行哪个扫描。
- 使用analyze table 表名 命令,可以重新统计索引信息。纠正错误结果
- 而优化器选择索引的目的,是找到一个最优的执行方案,并用最小的代价去执行语句。在数据库里面,扫描行数是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的次数越少,消耗的CPU资源越少。
-
另外一个语句:mysql> select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1
-
如果使用索引a进行查询,那么就是扫描索引a的前1000个值,然后取到对应的id,再到主键索引
上去查出每一行,然后根据字段b来过滤。显然这样需要扫描1000行。
如果使用索引b进行查询,那么就是扫描索引b的最后50001个值,与上面的执行过程相同,也是
需要回到主键索引上取值再判断,所以需要扫描50001行。 -
可以看到,返回结果中key字段显示,这次优化器选择了索引b,而rows字段显示需要扫描的行
数是50198。
从这个结果中,你可以得到两个结论:- 扫描行数的估计值依然不准确;
- 这个例子里MySQL又选错了索引。
-
原本可以执行得很快的SQL语句,执行速度却比你预期的慢很多,你应该怎么办呢?
- 一种方法是,像我们第一个例子一样,采用force index强行选择一个索引。
- 第二种方法就是,我们可以考虑修改语句,引导MySQL使用我们期望的索引。比如,在这个例子里,显然把“order by b limit 1” 改成“order by b,a limit 1” ,语义的逻辑是相同的。
- 之前优化器选择使用索引b,是因为它认为使用索引b可以避免排序(b本身是索引,已经是有序的了,如果选择索引b的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。
现在order by b,a 这种写法,要求按照b,a排序,就意味着使用这两个索引都需要排序。因此,扫描行数成了影响决策的主要条件,于是此时优化器选了只需要扫描1000行的索引a。
- 之前优化器选择使用索引b,是因为它认为使用索引b可以避免排序(b本身是索引,已经是有序的了,如果选择索引b的话,不需要再做排序,只需要遍历),所以即使扫描行数多,也判定为代价更小。
- 第三种方法是,在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。(删除索引b)
-
怎么给字符串字段加索引
-
场景例子:维护一个由邮箱构成的登录系统数据库
-
两种索引方式:
- alter table SUser add index index1(email);
- 这一种包含了每个记录的整个字符串
- alter table SUser add index index2(email(6));
- 对每个记录只取前6个字节
- 优点:占用空间小
- 增加额外的扫描次数
- alter table SUser add index index1(email);
-
如果使用index1扫描顺序
- sql语句:select id,name,email from SUser where email='hjjxyz@xxx.com'
- 先在index1索引树找到主键是ID2的行,判断email值是否正确,加入到结果集;
- 到主键上查找主键值是ID2的行,判断email是否正确,加入到结果集
- 在index1所引述刚刚查到的位置吓一跳记录,发现不满足条件,循环结束。
- 整个过程值回了一次表,可以认为只扫描了一行。
-
如果用index2顺序
- 在index2索引树找到满足索引值是hjjxyz的记录,找到了ID1
- 从主键上查找ID1的行,判断是否满足where条件,不是则丢弃
- 在index2查到的位置继续查找下一条,仍然是hjjxyz,取出,再到ID索引树判断,满足where条件,加入到结果集
- 重复上一步,直到index2不是hjjxyz,循环结束
- 在整个过程中,回表了多次,相当于扫描了多行
-
如果将index的长度变长,这样区分度更大,这样扫描的次数就减少了,但是占用了空间也变大了,那么如何控制好索引长度呢?
-
首先 select count(distinct email) as L from SUser;算出这个列有多少不同的值
-
查看4-7个字节的前缀索引,查出的个数
-
mysql> select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7, from SUser;
-
如果可以得到95%以上的数据时就可以选取该长度
-
-
前缀索引一定要回表,无法使用覆盖索引
- 如果用index1可以直接返回结果,不需要回表,但是如果是index2即使字段很长,innodb还是要回表查询。
-
如何最大化使用前缀索引提高搜索性能?
- 比如身份证,前6位如果是一个区的完全相同,区分度不高
- 倒叙存储 select field_list from t where id_card = reverse('身份证号码')
- 使用hash字段,在表上创建一个新字段,来hash保存身份证号的值,重复概率很小,可以认为每次只扫描了一行
- 这两种方式都不支持范围扫描