MySQL学习笔记(十六)——索引优化和查询优化

1. 前言

都有哪些维度可以进行数据库调优?简言之:

  • 索引失效、没有充分利用到索——索引建立
  • 关联查询太多JOIN(设计缺陷或不得已的需求)——SQL优化
  • 服务器调优及各个参数设置(缓冲、线程数等)――调整my.cnf
  • 数据过多――分库分表

虽然SQL查询优化的技术有很多,但是大方向上完全可以分成物理查询优化和逻辑查询优化两大块。

  • 物理查询优化是通过索引和表连接方式等技术来进行优化,这里重点需要掌握索引的使用。
  • 逻辑查询优化就是通过 SQL 等价变换提升查询效率,直白一点就是说,换一种查询写法执行效率可能更高。

2. 索引失效的11种案例

MysQL中提高性能的一个最有效的方式是对数据表设计合理的索引。索引提供了高效访问数据的方法,并且加快查询的速度,因此索引对查询的速度有着至关重要的影响。

  • 使用索引可以快速地定位表中的某条记录,从而提高数据库查询的速度,提高数据库的性能。
  • 如果查询时没有使用索引,查询语句就会扫描表中的所有记录。在数据量大的情况下,这样查询的速度会很慢。

大多数情况下都(默认)采用B+树来构建索引。只是空间列类型的索引使用R-树,并且MEMORY表还支持hash索引

其实,用不用索引,最终都是优化器说了算

优化器是基于什么优化?

  • 基于cost开销(CostBaseOptimizer),它不是基于规则(Rule-Based0ptimizer),也不是基于语义。怎么样开销小就怎么来

  • 另外,SQL语句是否使用索引,跟数据库版本、数据量、数据选择度都有关系

全值匹配我最爱

CREATE INDEX idx_age ON student(age);

CREATE INDEX idx_age_classid ON student(age,classId);

CREATE INDEX idx_age_classid_name ON student(age,classId,NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30; # 使用 idx_age

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4;# idx_age_classid_name
EXPLAIN SELECT SQL_NO_CACHE age,classId FROM student WHERE age=30 AND classId=4;# idx_age_classid

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age=30 AND classId=4 AND NAME = 'abcd'; # idx_age_classid_name

最佳左前缀法则

  • 在MysQL建立联合索引时会遵守最佳左前缀匹配原则,即最左优先,在检索数据时从联合索引的最左边开始匹配

  • 最佳左前缀法则针对是索引中字段顺序,而不是sql字段顺序,sql字段顺序和索引不一致,优化器会帮忙调整,不影响索引生效。

CREATE INDEX idx_age_classid_name ON student(age,classId,NAME);
 
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE classid=4 AND student.age=30 AND student.name = 'abcd'; # idx_age_classid_name 

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.name = 'abcd'; # idx_age_classid_name 中只用到了 age

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.classid=30 AND student.name = 'abcd'; # 未用到

结论:MysQL可以为多个字段创建索引,一个索引可以包括16个字段。对于多列索引,**过滤条件要使用索引必须按照索引建立时的顺序,依次满足,一旦跳过某个字段,索引后面的字段都无法被使用。**如果查询条件中没有使用这些字段中第1个字段时,多列(或联合)索引不会被使用。

主键插入顺序(递增)

数据页已经满了,再插进来咋办呢?

我们需要把当前页面分裂成两个页面,把本页中的一些记录移动到新创建的这个页中。页面分裂和记录移位意味着什么?

意味着:性能损耗!所以如果我们想尽量避免这样无谓的性能损耗,最好让插入的记录的主键值依次递增,这样就不会发生这样的性能损耗了。

所以我们建议:让主键具有AUTO_INCREMENT,让存储引擎自己为表生成主键,而不是我们手动插入

计算、函数、类型转换(自动或手动)导致索引失效

CREATE INDEX idx_name ON student(NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';# idx_name

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; # 未使用到索引 LEFT(student.name,3)不知道是哪个字段

CREATE INDEX idx_sno ON student(stuno);

EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno+1 = 900001; # 失效

EXPLAIN SELECT SQL_NO_CACHE id, stuno, NAME FROM student WHERE stuno = 900000; # idx_sno

类型转换导致索引失效

CREATE INDEX idx_name ON student(NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = 123; # 失效

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = '123'; # idx_name

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

  • 范围右边的列不能使用。比如:(<)、(=)、(>)、(>=)和between等
CREATE INDEX idx_age_classId_name ON student(age,classId,NAME);

EXPLAIN SELECT SQL_NO_CACHE * FROM student 
WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ; # idx_age_classId_name,但是name字段未用到索引,效果等同于下面sql

EXPLAIN SELECT SQL_NO_CACHE * FROM student 
WHERE student.age=30 AND student.name = 'abc' AND student.classId>20; 

要想避免上述情况,可以优化:

CREATE INDEX idx_age_classId_name ON student(age,NAME,classId);

EXPLAIN SELECT SQL_NO_CACHE * FROM student 
WHERE student.age=30 AND student.classId>20 AND student.name = 'abc' ; 
# 上下 sql 等同
EXPLAIN SELECT SQL_NO_CACHE * FROM student 
WHERE student.age=30 AND student.name = 'abc' AND student.classId>20; 

应用开发中范围查询,例如:金额查询,日期查询往往都是范围查询。应将查询条件放置where语句最后。

不等于(!= 或者<>)索引失效

CREATE INDEX idx_name ON student(NAME);
# 以下 sql 都失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name <> 'abc' ;

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name != 'abc' ;

is null可以使用索引,is not null无法使用索引

CREATE INDEX idx_age ON student(age);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL; 

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL; 

like以通配符%开头索引失效

CREATE INDEX idx_name ON student(NAME);
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE 'ab%'; # idx_name

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE '%ab%'; # 失效

OR 前后存在非索引的列,索引失效

CREATE INDEX idx_age ON student(age);

EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age = 10 OR classid = 100; # 失效

数据库和表的字符集统一使用utf8mb4

统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码不同的 字符集 进行比较前需要进行 转换 会造成索引失效

一般性建议

  • 对于单列索引,尽量选择针对当前query过滤性更好的索引
  • 在选择组合索引的时候,当前query中过滤性最好的字段在索引字段顺序中,位置越靠前越好
  • 在选择组合索引的时候,尽量选择能够包含当前query中的where子句中更多字段的索引
  • 在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面

总之,书写SQL语句时,尽量避免造成索引失效的情况。

3. 关联查询优化

  • 对于内连接来说,查询优化器可以决定谁作为驱动表,谁作为被驱动表出现的。
  • 对于内连接来讲,如果表的连接条件中只能有一个字段有索引,则有索引的字段所在的表会被作为被驱动表出现。
  • 对于内连接来说,在两个表的连接条件都存在索引的情况下,会选择小表作为驱动表。“小表驱动大表”
  • 保证被驱动表的JOIN字段已经创建了索引
  • 需要JOIN 的字段,数据类型保持绝对一致。
  • LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
  • INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
  • 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
  • 不建议使用子查询,建议将子查询SQL拆开结合程序多次查询,或使用 JOIN 来代替子查询。
  • 衍生表建不了索引

4. 子查询优化

MySQL从4.1版本开始支持子查询,使用子查询可以进行SELECT语句的嵌套查询,即一个SELECT查询的结果作为另一个SELECT语句的条件。 子查询可以一次性完成很多逻辑上需要多个步骤才能完成的SQL操作 。子查询是 MySQL 的一项重要的功能,可以帮助我们通过一个 SQL **语句实现比较复杂的查询。但是,子查询的执行效率不高。**原因:

  • 执行子查询时,MySQL需要为内层查询语句的查询结果 建立一个临时表 ,然后外层查询语句从临时表中查询记录。查询完毕后,再 撤销这些临时表 。这样会消耗过多的CPU和IO资源,产生大量的慢查询。
  • 子查询的结果集存储的临时表,不论是内存临时表还是磁盘临时表都 不会存在索引 ,所以查询性能会受到一定的影响。
  • 对于返回结果集比较大的子查询,其对查询性能的影响也就越大。

**在MySQL中,可以使用连接(JOIN)查询来替代子查询。**连接查询 不需要建立临时表 ,其 速度比子查询要快 ,如果查询中使用索引的话,性能就会更好。

结论:尽量不要使用NOT IN 或者 NOT EXISTS,用LEFT JOIN xxx ON xx WHERE xx IS NULL替代

5. 排序优化

  • 在 WHERE 条件字段上加索引,但是为什么在 ORDER BY 字段上还要加索引呢?
    • SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫描在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
    • 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列;如果不同就使用联合索引。
    • 无法使用 Index 时,需要对 FileSort 方式进行调优

6. 优先考虑覆盖索引

什么是覆盖索引

理解方式一:索引是高效找到行的一个方法,但是一般数据库也能使用索引找到一个列的数据,因此它不必读取整个行。毕竟索引叶子节点存储了它们索引的数据;当能通过读取索引就可以得到想要的数据,那就不需要读取行了。一个索引包含了满足查询结果的数据就叫做覆盖索引

理解方式二:非聚簇复合索引的一种形式,它包括在查询里的SELECT、JOIN和WHERE子句用到的所有列(即建索引的字段正好是覆盖查询条件中所涉及的字段)。

简单说就是, 索引列+主键 包含 SELECT 到 FROM之间查询的列

覆盖索引的利弊

  • 好处:

    • 避免Innodb表进行索引的二次查询(回表)
    • 可以把随机IO变成顺序IO加快查询效率
  • 弊端:

    • 索引字段的维护 总是有代价的。因此,在建立冗余索引来支持覆盖索引时就需要权衡考虑了。

7. 如何给字符串添加索引

MySQL是支持前缀索引的,也就是说,你可以定义字符串的一部分作为索引。默认地,如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字

符串。

mysql> alter table teacher add index index1(email);
#或
mysql> alter table teacher add index index2(email(6));

**使用前缀索引,定义好长度,就可以做到既节省空间,又不用额外增加太多的查询成本。**前面已经讲过区分度,区分度越高越好。因为区分度越高,意味着重复的键值越少。

结论:使用前缀索引就用不上覆盖索引对查询性能的优化了,这也是你在选择是否使用前缀索引时需要考虑的一个因素。

8. 索引下推

Index Condition Pushdown(ICP)是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。

索引下推是把本应该在 server 层进行筛选的条件,下推到存储引擎层来进行筛选判断,这样能有效减少回表。

  • 如果没有ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给MysQL服务器,由MysQL服务器评估WHERE后面的条件是否保留行。
  • 启用ICP后,如果部分WHERE条件可以仅使用索引中的列进行筛选,则MySQL服务器会把这部分WHERE条件放到存储引擎筛选。然后,存储引擎通过使用索引条目来筛选数据,并且只有在满足这一条件时才从表中读取行。
    • 好处: ICP可以减少存储引擎必须访问基表的次数和MySQL服务器必须访问存储引擎的次数
    • 但是,ICP的加速效果取决于在存储引擎内通过ICP筛选掉的数据的比例。

ICP的开启和关闭

默认情况下启用索引条件下推。可以通过设置系统变量 optimizer_switch控制:index_condition_pushdown

#关闭索引下推
SET optimizer_switch = 'index_condition_pushdown=off';
#打开索引下推
SET optimizer_switch = 'index_condition_pushdown=on';

当使用索引条件下推时,EXPLAIN语句输出结果中Extra列内容显示为Using index condition

例如:

EXPLAIN SELECT * FROM people
WHERE zipcode='000001'
AND lastname LIKE '%张%'
AND address LIKE '%北京市%';
# 上面如果不使用索引下推,将只会用到zipcode就回表
EXPLAIN SELECT * FROM people
WHERE zipcode='000001'
AND lastname LIKE '张%'
AND firstname LIKE '三%';

id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
1	SIMPLE	people	\N	range	zip_last_first	zip_last_first	189	\N	1	25.00	Using index condition

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优化方法。

9. 其他查询策略优化

EXISTS和IN的区分

问题:

不太理解哪种情况下应该使用EXISTS,哪种情况应该用IN。选择的标准是看能否使用表的索引吗?

回答:

索引是个前提,其实选择与否还是要看表的大小。你可以将选择的标准理解为小表驱动大表。在这种方式下效率是最高的。

COUNT(*)与COUNT(具体字段)效率

问:在MySQL中统计数据表的行数,可以使用三种方式: SELECT COUNT(*)、SELECT COUNT(1)和SELECT COUNT(具体字段),使用这三者之间的查询效率是怎样的?

答:

前提:如果你要统计的是某个字段的非空数据行数,则另当别论,毕竟比较执行效率的前提是结果一样才可以。

环节1: **COUNT(*)和COUNT(1)都是对所有结果进行COUNT,COUNT(*)和COUNT(1)**本质上并没有区别(二者执行时间可能略有差别,不过你还是可以把它俩的执行效率看成是相等的)。如果有WHERE子句,则是对所有符合筛选条件的数据行进行统计;如果没有WHERE子句,则是对数据表的数据行数进行统计。

环节2:如果是MylSAM存储引擎,统计数据表的行数只需要O(1)的复杂度,这是因为每张MyISAM的数据表都有一个meta信息存储了row_count值,而一致性则由表级锁来保证。

如果是InnoDB存储引擎,因为InnoDB支持事务,采用行级锁和MVCC机制,所以无法像MylSAM一样,维护一个row_count变量,因此需要采用扫描全表,是o(n)的复杂度,进行循环+计数的方式来完成统计。

环节3:在InnoDB引擎中,如果采用COUNT(具体字段)来统计数据行数,要尽量采用二级索引。因为主键采用的索引是聚簇索引,聚簇索引包含的信息多,明显会大于二级索引(非聚簇索引)。对于COUNT(*)和COUNT(1)来说,它们不需要查找具体的行,只是统计行数,系统会自动采用占用空间更小的二级索引来进行统计。如果有多个二级索引,会使用key_len小的二级索引进行扫描。当没有二级索引的时候,才会采用主键索引来进行统计。

关于SELECT(*)

在表查询中,建议明确字段,不要使用*作为查询的字段列表,推荐使用SELECT <字段列表>查询

原因:

  • MySQL在解析的过程中,会通过查询数据字典将"*"按序转换成所有列名,这会大大的耗费资源和时间。
  • 无法使用覆盖索引

LIMIT 1对优化的影响

针对的是会扫描全表的SQL语句,如果你可以确定结果集只有一条,那么加上LIMIT 1的时候,当找到一条结果的时候就不会继续扫描了,这样会加快查询速度。

如果数据表已经对字段建立了唯一索引,那么可以通过索引进行查询,不会全表扫描的话,就不需要加上LIMIT1了

多使用COMMIT

只要有可能,在程序中尽量多使用COMMIT,这样程序的性能得到提高,需求也会因为COMMIT所释放的资源而减少
COMMIT所释放的资源:

  • 回滚段上用于恢复数据的信息
  • 被程序语句获得的锁
  • redo / undo log buffer中的空间
  • 管理上述3种资源中的内部花费

参考

https://www.bilibili.com/video/BV1iq4y1u7vj/?spm_id_from=333.337.search-card.all.click&vd_source=25b05e9bd8b4bdac16ca2f47bbeb7990

猜你喜欢

转载自blog.csdn.net/qq_42130468/article/details/130025797