MySQL的InnoDB索引失效的场景和优化

发布时间 2023-10-16 14:16:02作者: 长寿奉孝

康师傅YYDS

索引失效案例

索引最好是全值匹配。

where条件中等值比,同时where的条件一起创建联合索引。

最佳左前缀

如果有一个联合索引,要想使用到,需要从联合索引的最左边的字段开始写,一点一点的写上。

主键插入顺序

在InnoDB中,使用自增的主键。避免索引页面分裂

计算、函数、类型转换(自动、手动)

函数:

1 CREATE INDEX idx_student_name ON student(name);
3 // type = range
4 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
6 // type = ALL
7 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';

 

计算:

1 CREATE INDEX idx_student_stuno ON student(stuno);
2 // type = ALL
3 EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001;

 

类型转换:

1 // type = ALL
2 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123;
3 // type = ref
4 EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123';

 

范围条件右边的列索引失效

几个列联合索引,前面的查范围里,后面的就用不到了。在开发中,创建联合索引时,将常常用于范围查找的如:金额、日期等放在联合索引的最后面

不等于 !=  <>用不上索引
IS NULL 可以用索引  IS NOT NULL不能用索引

最好在设计数据表的时候,将字段设置为not null,比如int字段,设置默认值为0,字符串设置默认为空字符串。

not like 、like以%开头 也不能使用索引
拓展:Alibaba《Java开发手册》
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
OR 前后两个条件存在非索引的列,索引失效
数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。
不同的 字符集 进行比较前需要进行 转换 会造成索引失效。
 

 

 

关联查询优化

左外连接

全连接是合并、左外连接和右外连接可以互相转换

LEFT JOIN 条件用于确定如何从右表搜索行,左边一定都有(从左边取一条数据,然后就去右边表找这条数据),
所以 右边是我们的关键点,一定需要建立索引 。 

 

内连接

内连接需要两张表公共的,没有那张表全都要的情况,两张表的地位相同,优化器会选择一个作为驱动表。

如果两张表连接条件中,只有一个表中这个字段有索引,那么这个有索引的会被作为被驱动表。

两个表的连接都存在索引的情况下,会选择数据量小的作为驱动表。(小表驱动大表)

 

小结
保证被驱动表的JOIN字段已经创建了索引
需要JOIN 的字段,数据类型保持绝对一致。
LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
衍生表建不了索引

 

子查询优化

子查询效率不高

原因:
① 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
② 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会受到一定的影响。
③ 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。 
 
 
在MySQL中,可以使用连接(JOIN)查询来替代子查询。连接查询 不需要建立临时表 ,其 速度比子查询要快 ,如果查询中使用索引的话,性能就会更好。
结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代 。

排序优化

优化建议:
1. SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫
描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排
序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
2. 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;
如果不同就使用联合索引。
3. 无法使用 Index 时,需要对 FileSort 方式进行调优
  3.1 尝试提高 sort_buffer_size
  3.2 尝试提高 max_length_for_sort_data
  3.3 Order by 时select * 是一个大忌。最好只Query需要的字段。
INDEX a_b_c(a,b,c)
order by 能使用索引最左前缀
- ORDER BY a
- ORDER BY a,b
- ORDER BY a,b,c
- ORDER BY a DESC,b DESC,c DESC
如果WHERE使用索引的最左前缀定义为常量,则order by 能使用索引
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b = const ORDER BY c
- WHERE a = const ORDER BY b,c
- WHERE a = const AND b > const ORDER BY b,c
不能使用索引进行排序
- ORDER BY b,c,a
- ORDER BY a ASC,b DESC,c DESC /* 排序不一致 */ - WHERE g = const ORDER BY b,c /*丢失a索引*/ - WHERE a = const ORDER BY c /*丢失b索引*/ - WHERE a = const ORDER BY a,d /*d不是索引的一部分*/ - WHERE a in (...) ORDER BY b,c /*对于排序来说,多个相等条件也是范围查询*/
结论:type 是 ALL,即最坏的情况。Extra 里还出现了 Using filesort,也是最坏的情况。优化是必须的。
order的时候不使用limit,有可能有索引但是优化器不走,因为优化器发现数据挺多,走了二级索引之后又要回表操作,不如直接内存中排序。

分组、分页优化

group by
group by 使用索引的原则几乎跟order by一致 ,group by 即使没有过滤条件用到索引,也可以直接使用索引。
group by 先排序再分组,遵照索引建的最佳左前缀法则当无法使用索引列,增大 max_length_for_sort_data 和 sort_buffer_size 参数的设置
where效率高于having,能写在where限定的条件就不要写在having中了
减少使用order by,和业务沟通能不排序就不排序,或将排序放到程序端去做。
Order by、group by、distinct这些语句较为耗费CPU,数据库的CPU资源是极其宝贵的。
包含了order by、group by、distinct这些查询的语句,where条件过滤出来的结果集请保持在1000行以内,否则SQL会很慢。
limit

一般分页查询时,通过创建覆盖索引能够比较好的提高性能。头疼的是limit 2000000,10  此时需要排序出前2000010条记录,然后仅仅返回2000000-2000010的记录,其他记录丢弃了,查询排序的代价非常大。

1、在索引上完成排序分页操作,最后根据主键关联回原表查询所需要的其他列内容。
1 EXPLAIN SELECT * FROM student t,(SELECT id FROM student ORDER BY id LIMIT 2000000,10) a
2 WHERE t.id = a.id;

 2、该方案适用于主键自增的表,可以把Limit 查询转换成某个位置的查询 。

1 EXPLAIN SELECT * FROM student WHERE id > 2000000 LIMIT 10;

 

覆盖索引

概念
理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;
  当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引。
理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。
简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列 。
覆盖索引的利弊
好处:
1. 避免Innodb表进行索引的二次查询(回表)
2. 可以把随机IO变成顺序IO加快查询效率
弊端:
索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。这是业务
DBA,或者称为业务数据架构师的工作。
 
给字符串创建前缀索引;
使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。
 
区分度
前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。
alter table teacher add index index1(email);
#或
alter table teacher add index index2(email(6));
使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。

索引(条件)下推

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
ICP的使用条件:
① 只能用于二级索引(secondary index)
②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。
SELECT * FROM s1 WHERE key1 > 'z' AND key1 like'%a'

 

其他策略

 
change buffer的使用场景
1. 普通索引和唯一索引应该怎么选择?其实,这两类索引在查询能力上是没差别的,主要考虑的是对 更新性能 的影响。所以,建议你 尽量选择普通索引 。
2. 在实际使用中会发现, 普通索引 和 change buffer 的配合使用,对于 数据量大 的表的更新优化还是很明显的。
3. 如果所有的更新后面,都马上 伴随着对这个记录的查询 ,那么你应该 关闭change buffer 。而在其他情况下,change buffer都能提升更新性能。
4. 由于唯一索引用不上change buffer的优化机制,因此如果 业务可以接受 ,从性能角度出发建议优先考虑非唯一索引。但是如果"业务可能无法确保"的情况下,怎么处理呢?
首先, 业务正确性优先 。我们的前提是“业务代码已经保证不会写入重复数据”的情况下,讨论性能问题。如果业务不能保证,或业务就是要求数据库来做约束,那么没得选,必须创建唯一索引。这种情况下,本节的意义在于,如果碰上了大量插入数据慢、内存命中率低的时候,给你多提供一个排查思路。然后,在一些“ 归档库 ”的场景,你是可以考虑使用唯一索引的。比如,线上数据只需要保留半年,然后历史数据保存在归档库。这时候,归档数据已经是确保没有唯一键冲突了。要提高归档效率,可以考虑把表里面的唯一索引改成普通索引。
 
EXISTS 和 IN 的区分
问题:不太理解哪种情况下应该使用 EXISTS,哪种情况应该用 IN。选择的标准是看能否使用表的索引吗?
COUNT(*)与COUNT(具体字段)效率
问:在 MySQL 中统计数据表的行数,可以使用三种方式: SELECT COUNT(*) 、 SELECT COUNT(1) 和SELECT COUNT(具体字段) ,使用这三者之间的查询效率是怎样的?
关于SELECT(*)
在表查询中,建议明确字段,不要使用 * 作为查询的字段列表,推荐使用SELECT <字段列表> 查询。原因:
① MySQL 在解析的过程中,会通过 查询数据字典 将"*"按序转换成所有列名,这会大大的耗费资源和时间。
② 无法使用 覆盖索引
 
 
LIMIT 1 对优化的影响
针对的是会扫描全表的 SQL 语句,如果你可以确定结果集只有一条,那么加上 LIMIT 1 的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。
如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上 LIMIT 1 了。
多使用COMMIT
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少。
COMMIT 所释放的资源:
  回滚段上用于恢复数据的信息
  被程序语句获得的锁
  redo / undo log buffer 中的空间
  管理上述 3 种资源中的内部花费
 

自增ID的问题

自增ID做主键,简单易懂,几乎所有数据库都支持自增类型,只是实现上各自有所不同而已。自增ID除了简单,其他都是缺点,总体来看存在以下几方面的问题:
1. 可靠性不高
存在自增ID回溯的问题,这个问题直到最新版本的MySQL 8.0才修复。
2. 安全性不高
对外暴露的接口可以非常容易猜测对应的信息。比如:/User/1/这样的接口,可以非常容易猜测用户ID的值为多少,总用户数量有多少,也可以非常容易地通过接口进行数据的爬取。
3. 性能差
自增ID的性能较差,需要在数据库服务器端生成。
4. 交互多
业务还需要额外执行一次类似 last_insert_id() 的函数才能知道刚才插入的自增值,这需要多一次的网络交互。在海量并发的系统中,多1条SQL,就多一次性能上的开销。
5. 局部唯一性
最重要的一点,自增ID是局部唯一,只在当前数据库实例中唯一,而不是全局唯一,在任意服务器间都是唯一的。对于目前分布式系统来说,这简直就是噩梦。
 
经验:
刚开始使用 MySQL 时,很多人都很容易犯的错误是喜欢用业务字段做主键,想当然地认为了解业务需求,但实际情况往往出乎意料,而更改主键设置的成本非常高。