MySQL执行计划与索引优化全面解析(三)

​ 本文系统讲解了数据库执行计划的核心概念、获取方法、关键指标解读及索引优化策略。详细阐述了执行计划中各字段的含义,重点分析了索引应用类型(type)、索引覆盖长度(key_len)的计算与解读,深入探讨了联合索引的应用原则和优化技巧。同时介绍了MySQL 8.0新增的索引特性及数据库自主优化机制(AHI、Change Buffer、ICP、MRR),为数据库性能调优提供完整解决方案。

1.执行计划概念

​ 执行计划是数据库优化器选择的最优SQL语句执行方案,展示了SQL查询的数据访问路径、过滤条件和结果获取方式。通过分析执行计划,可以预判SQL语句的执行行为和性能表现。

​ 在介绍数据库服务程序运行逻辑时,在SQL层处理SQL语句时,会根据解析器生成解析树(多种处理方案);然后在利用优化器生成最终的执行计划,然后在根据最优的执行计划进行执行SQL语句;作为管理员,可以在某个语句执行前,将语句对应的执行计划提取出来进行分析,便可大体判断语句的执行行为,从而了解执行效果;

可以简单理解:执行计划就是最优的一种执行SQL语句的方案,表示相应SQL语句是如何完成的数据查询与过滤,以及获取;

2.利用命令获取执行计划信息

sh 复制代码
 explain select * from liux.t100w where k2='VWlm';
 或者
 desc select * from liux.t100w where k2='VWlm';

#给k2列加辅助索引
mysql> alter table t100w add index k2_index(k2);
mysql> desc select * from liux.t100w where k2='VWlm';

输出信息解释说明:

序号 字段 解释说明
01列 ID 表示语句执行顺序,单表查询就是一行执行计划,多表查询就会多行执行计划;
02列 select_type 表示语句查询类型,sipmle表示简单(普通)查询
03列 table 表示语句针对的表,单表查询就是一张表,多表查询显示多张表;
05列 ⭐️type 表示索引应用类型,通过类型可以判断有没有用索引,其次判断有没有更好的使用索引
06列 possible_keys 表示可能使用到的索引信息,因为列信息是可以属于多个索引的
07列 key 表示确认使用到的索引信息
08列 key_len 表示索引覆盖长度,对联合索引是否都应用做判断
10列 rows 表示查询扫描的数据行数(尽量越少越好),尽量和结果集行数匹配,从而使查询代价降低
11列 fltered 表示查询的匹配度
12列 ⭐️Extra 表示额外的情况或额外的信息

3.索引应用类型(type)

利用类型信息,来判断确认索引的扫描方式,常见的索引扫描方式类型:

序号 类型 解释说明
01 ALL 表示全表扫描方式,没有利用索引扫描类型;
02 index 表示全索引扫描方式,需要将索引树全部遍历,才能获取查询的信息(主键index=全表扫描)
03 range 表示范围索引方式,按照索引的区域范围扫描数据,获取查询的数据信息;
04 ref 表示辅助索引等值(常量)查询,精准定义辅助索引的查询条件
05 eq_ref 表示多表连接查询时,被驱动表的连接条件是主键或者唯一键时,获取的数据信息过程;
06 const/system 表示主键或者唯一键等值(常量)查询,精准定义索引的查询条件
sh 复制代码
1.all-全表扫描
#此类型出现原因一:查找条件没有索引;
mysql> desc select * from  liux.t100w where k1='lm';

#此类型出现原因二:查询条件不符合查询规律(like %%-只针对辅助索引,不影响主键索引-range);
mysql> desc select * from liux.t100w where k2 like '%ma%';

#此类型出现原因三:查询条件使用的了排除法(!=/not in-只针对辅助索引,不影响主键索引);
mysql> desc select * from liux.t100w where k2 not in ('ma','wwee','ccee');

2.index-等价于全表扫描
#此类型出现原因:扫描查询列设置了索引信息,但是没有基于索引列设置查询条件
mysql> desc select k2 from liux.t100w;

-- all、index方式不推荐

3.range-范围查询
#此类型出现原因:查找条件是范围信息(> < >= <= between and in or)
mysql> desc select * from liux.t100w where k2  in ('ma','wwee','ccee');
特殊说明:在利用in查询数据信息时,查询效果和逻辑语句or的查询效果是一致;

#此类型出现原因:查找条件是模糊信息(like)
mysql> desc select * from liux.t100w where k2 like 'ma%';

4.ref
#此类型出现原因:查找条件是精确等值信息
mysql> desc select * from liux.t100w where k2='ma';

5.eq_ref
#此类型出现原因:被驱动表的链表条件是主键或唯一键时
MySQL驱动表和被驱动表说明:https://www.cnblogs.com/liux666/p/16892774.html
mysql> desc select city.name,country.name,city.population from city join country on city.countrycode=country.code;
当连接查询没有where条件时:
左连接查询时,前面的表是驱动表,后面的表是被驱动表,右连接查询时相反;
内连接查询时,哪张表的数据较少,哪张表就是驱动表

    mysql> desc select city.name,country.name,city.population from city join country on city.countrycode=country.code where city.population<100;
当连接查询有where条件时,带where条件的表是驱动表,否则是被驱动表
说明:在没有设置比较合理索引情况下,默认选择结果集小的作为驱动表,即小表驱动大表;

给population加上索引查询最优:
mysql> alter table city add index idx_pop(population);

mysql> desc select city.name,country.name,city.population from city join country on city.countrycode=country.code where city.population<100;

6.const
#此类型出现原因:查询的数据条件是主键或唯一键,并且是精确等值查询;
mysql> desc select * from city where id=10;

4.索引覆盖长度

在执行计划列中,key_len主要用来判断联合索引覆盖长度(字节),当覆盖长度越长,就表示匹配度更高,回表查询的次数越少;

到底联合索引被覆盖了多少,是可以通过key_len计算出来;

sh 复制代码
# 联合索引设置
alter table t1 add index id_a_b_c(a列,b列,c列);
# 联合索引应用
select * from t1 where a=xx and b=xx and c=xx;
100行 -- 回表100
50行  -- 回表50
10行  -- 回表10

如果全部覆盖到了:长度=a+b+c 即三个列最大预留长度的总和

最大预留长度影响因素?

  • 数据类型:
  • 字符集(GBK:中文每个字符占用2个字节,英文1个字节 /UTF-8:中文每个字符占用3个字节,英文1个字节)
  • not null 是否可以为空 name

最大预留长度计算结果:不同的数据类型

数据类型 字符集 计算结果
char(10) utf8mb4 最大预留长度=4*10=40
utf8 最大预留长度=3*10=30
varcher(10) utf8mb4 最大预留长度=4*10=40 + 2字节 =42 (1-2字节存储字符长度信息)
utf8 最大预留长度=3*10=30 + 2字节 =32 (1-2字节存储字符长度信息)
tinyint N/A 最大预留长度=1(大约3位数) 2的8次方=256
int N/A 最大预留长度=4(大约10位数) 2的32次方=4294967296
bigint N/A 最大预留长度=8(大约20位数) 2的64次方=18446744073709551616
not null N/A 在没有设置not null时,在以上情况计算结果再+1

实例操作练习:理解key_len索引覆盖长度

sh 复制代码
# 常见测试数据表
use world;
create table keylen (
    id int not null primary key auto_increment,
    k1 int not null,
    k2 char(20),
    k3 varchar(30) not null,
    k4 varchar(10)
) charset=utf8mb4;

# 设置表中列索引信息
alter table keylen add index idx(k1,k2,k3,k4);

mysql> desc keylen;
mysql> show index from keylen;


当四个索引信息全部覆盖,key_len数值计算结果:
# key_len计算思路
k1 = 4
k2 = 4 * 20 +1 = 81
k3 = 4 * 30 +2 = 122
k4 = 4 * 10 +2 + 1 = 43
sum = 4 + 81 + 122 + 43 = 250

# 进行校验结果
desc select * from keylen where k1=1 and k2='a' and k3='a' and k4='a';

说明:根据key_len长度数值,理想上是和联合索引的最大预留长度越匹配越好,表示索引都用上了,回表次数自然会少;

5.联合索引应用

5.1 联合索引全部覆盖
  • 需要满足最左原则;(尽量)
  • 需要定义条件信息时,将所有联合索引条件都引用;(必要)
sh 复制代码
mysql> use liux;
mysql> show index from t100w;
mysql> alter table t100w drop index idx_k2;
mysql> show index from t100w;
mysql> desc t100w;
-- 删除原有表中所有索引信息;

# 在不满足最左原则创建联合索引
mysql> alter table t100w add index idx(num,k1,k2);
-- 此时key_len的最大预留长度:4+1 + 2*4+1 + 4*4+1 = 31

验证索引全覆盖最大预留长度
desc select * from t100w where num=913759 and k1='ej' and k2='EFfg';

说明:进行联合索引全覆盖时,索引条件的应用顺序是无关的,因为优化器会自动优化索引查询条件应用顺序;

sh 复制代码
#获取重复数据信息
mysql> select num,count(*) from t100w group by num having count(*)>1 order by count(*) desc limit 3;

#进行范围索引全覆盖查询
desc select * from t100w where num=339934 and k1='yb' and k2 > 'PQqr';

说明:在进行联合索引全覆盖查询时,**最后一列**不是精确匹配查询,而是采取区间范围查询,也可以实现索引全覆盖查询效果;

5.2 联合索引部分覆盖
  • 需要满足最左原则;
  • 需要定义条件信息时,将所有联合索引条件部分引用;
sh 复制代码
mysql> desc select * from t100w where num=339934;
mysql > desc  select * from t100w where num=339934 and k1<'yb' and k2='nokl';
说明:进行联合索引覆盖查询时,区间范围列不是最后一列,索引查询匹配只统计到区间范围匹配(不等值)列,也属于部分覆盖;

desc select * from t100w where num=339934  and k2='ej';
说明:进行联合索引覆盖查询时,查询索引列是不连续的,索引查询匹配只统计到缺失列前,也属于部分覆盖;
5.3 联合索引完全不覆盖
  • 需要定义条件信息时,将所有联合索引条件都不做引用;
sh 复制代码
mysql> desc select * from t100w;

mysql> desc select * from t100w where num<339934 ;
说明:进行联合索引全不覆盖查询时,区间范围列出现在了第一列,也属于全不覆盖索引

mysql> desc select * from t100w where k2='ej';
说明:进行联合索引全不覆盖查询时,缺失最左列索引条件信息时,也属于全不覆盖索引
5.4 联合索引最左原则压力测试
sh 复制代码
测试情况一:在不满足最左选择度高的情况;
# 创建索引情况
mysql> alter table t100w add index idx(num,k1,k2);

# 执行压力测试命令
[root@db01 ~]# mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='liux' --query="select * from t100w where num=339934 and k1='yb' and k2='PQqr';" engine=innodb --number-of-queries=200000 -uroot -p12366 -h10.0.0.51 -verbose
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
	Running for engine rbose
	Average number of seconds to run all queries: 30.345 seconds
	Minimum number of seconds to run all queries: 30.345 seconds
	Maximum number of seconds to run all queries: 30.345 seconds
	Number of clients running queries: 100
	Average number of queries per client: 2000


测试情况二:在满足最左选择度高的情况;
# 调整索引情况
mysql> alter table t100w drop index idx;
mysql> alter table t100w add index idx(k1,k2,num);

# 执行压力测试命令
[root@db01 ~]# mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='liux' --query="select * from t100w where num=339934 and k1='yb' and k2='PQqr';" engine=innodb --number-of-queries=200000 -uroot -p12366 -h10.0.0.51 -verbose
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
	Running for engine rbose
	Average number of seconds to run all queries: 30.852 seconds
	Minimum number of seconds to run all queries: 30.852 seconds
	Maximum number of seconds to run all queries: 30.852 seconds
	Number of clients running queries: 100
	Average number of queries per client: 2000

6.索引扩展信息

Extar列表示额外的情况或额外的信息说明,其中重点需要关注点信息为:filesort 表示涉及到额外排序操作,将严重浪费CPU资源;

哪些查询语句情况涉及到排序操作:

  • 情况一:查询语句中含有 order by ,表示触发式的排序;
  • 情况二:查询语句中含有 group by,表示隐藏式的排序;
  • 情况三:查询语句中含有 DISTINCT,表示会先进行排序后再取消重复;
sh 复制代码
# 查看指定表索引信息
mysql> use world;
mysql> show index from city;

# 删除无用索引信息
mysql> alter table city drop index idx_pop;

#利用辅助索引信息作为条件,查看所有中国的城市情况信息:
mysql> desc select * from city where countrycode='CHN';

# 模拟情况一:利用order by实现排序
mysql> desc select * from city where countrycode='CHN' order by population;

# 错误设想创建索引:因为本身索引构建过程就存在自动排序问题
alter table city add index idx(population);

# 正确优化处理方式:创建联合索引
mysql> alter table city add index idx1(countrycode,population);
mysql> desc select * from city where countrycode='CHN' order by population;


特殊情况说明:在order by信息出现在group by之后,是无法实现索引优化处理的
# 模拟情况二:利用group by实现排序
mysql> desc select district,count(*) from city where countrycode='CHN' group by district;
mysql> desc select district,count(*) from city where countrycode='CHN' group by district order by sum(population);

7.索引创建规范

  • 数据表中必须要有主键索引(创建表时指定),建议是与业务无关的自增列;

  • 数据表中某些列若经常作为 where/order by/group by/join on/distinct条件信息,最好将相应列设置索引(产品功能/用户行为)

  • 数据表中最好使用唯一值多的列作为索引,如果索引列重复值较多,可以考虑使用联合索引;(最左列-减少回表次数 - 减少磁盘IO)

  • 数据表中列值长度较长的索引列,建议可以使用前缀索引;(防止索引树层次过高)

  • 数据表中不建议建立大量索引,最好降低索引条目,不要创建无用索引,不常用的索引要定期清理(percona toolkit)

  • 数据表中的索引信息做调整维护时,尽量避开业务繁忙期,或者通过软件工具做调整维护(pt-ost)

  • 数据表中的联合索引创建过程要遵循索引最左原则;

8.索引知识扩展(8.0新增)

8.1 支持不可见索引功能
sh 复制代码
# 在创建索引或修改索引时,可以设置不可见或可见索引(默认)
mysql> alter table test alter index idx invisible;
mysql> alter table test add index idx1(name) invisible;
在做批量数据导入时,辅助索引信息可以设置为不可见,优化器就不会加载识别索引信息
8.2 支持倒序索引功能
sh 复制代码
# 官方解释说明
idx(a,b,c)
-- 创建a b c 索引列 并按照从小到大排序
desc select * from where xx order by a,b desc,c   索引全覆盖

order by a,b desc,c
-- 由于排序中出现了逆向排序,所以只有a列会走索引,查询b和c还是会再进行排序处理,不会利用索引排序

# 最新版数据库索引创建
idx(a,b desc,c)
-- 可以灵活调整索引排序方式,应对不同的查询条件,从而避免排序问题对CPU资源的消耗

9 数据库自主优化能力

9.1 AHI(索引的索引)

AHI全称(中文名称)为自适应的hash索引/散列索引,用于在内存中建立索引,快速锁定内存中的热点数据索引页位置;

正常情况下,所有数据都是存储在磁盘中的,如果想访问读取相应磁盘的数据信息,都是会将磁盘数据调取存放在内存中,即消耗IO;

对于数据库服务而言,想要读取数据信息,也是会从磁盘中读取存储页,在放入内存中被数据库服务进行访问,索引访问也是一样的;

但是当数据页大量的被存放在内存中后,从大量内存中的数据页找到想要的,也是比较困难的事情;

因此,可以对内存中经常被访问数据索引页建立一个hash索引,从而可以帮助数据库服务快速定位内存中想要找的索引数据页;

sh 复制代码
AHI功能配置信息:
mysql> show variables like 'innodb_adaptive_hash_index';
9.2 change buffer

早期版本称为 insert buffer,只是对插入操作有作用,版本更新后(5.6),可以对插入 修改 删除操作都有作用效果;

change buffer主要是针对辅助索引的缓冲区,属于内存结构上的应用;

changerbuffer应用原理:假设现在需要插入一行数据信息

① 插入一行数据信息到表中,将会实时立即更新聚簇索引信息,因为利用聚簇索引是用来获取数据页上详细原表数据信息的;

② 插入一行数据信息到表中,不会实时立即更新辅助索引信息,因为利用辅助索引是用来获取索引页上聚簇索引数据信息的;

​ 如果此时实时更新了辅助索引的信息,有可能会导致出现数据页分裂,造成辅助索引树结构变化,形成索引树访问阻塞(锁机制);

③ 为了避免辅助索引树结构变更,对数据库服务并发访问的影响,可以将插入的数据信息,暂时存储在缓冲区中;

​ 当利用辅助索引检索数据时,可以将检索到数据页范围信息调取到内存中,与缓存区数据进行合并,自然可以检索到插入的数据;

说明:在数据表中插入 修改 删除数据时,聚簇索引树会进行同步实时更新,辅助索引树会进行异步延时更新。

sh 复制代码
change_buffer功能配置信息:
mysql> show variables like '%change_buffer%';

--all:	  默认值。开启buffer inserts、delete-marking operations、purges
--none:	不开启change buffer
9.3 ICP (索引下推)

属于5.6之后引用的数据库服务新特性,称之为索引下推功能,主要是针对联合索引功能起作用;

ICP应用原理:假设创建联合索引进行数据检索

sh 复制代码
idx(a,b,c)
where a=10 and b like '%x%' and c=z 

在没有ICP优化机制情况:

基于联合索引的特性,查找检索数据只会依据a进行检索,可能检索到的数据页是100个数据块,会将数据放入内存中;

数据信息到达内存中后,在根据b和c的条件信息进行定位最终的聚簇索引信息,进行回表查询;

说明:基于数据库优化器的特性,遵循联合索引引用原则,SQL层面只能检索到联合索引中的A;

在应用ICP优化机制情况:

基于联合索引的特性,查找检索数据只会依据a进行检索,但是b和c也属于联合索引中的索引部分,在SQL层不能再进行索引情况下;

可以将b和c的检索工作下推交给引擎层完成,可以让引擎再调取数据到内存之前,再根据b和c的条件进行一次过滤;

可以将过滤后的数据信息再放入到内存中,然后结合获取到的聚簇索引信息,进行回表查询;

说明:基于数据库优化器的特性,可以将SQL层完成不了的检索工作,下推给引擎层完成,从而减少磁盘IO消耗,以及回表策略

sh 复制代码
mysql> show variables like '%switch%';
mysql> set global optimizer_switch='index_condition_pushdown=off';
-- 实现测试练习完,需要恢复开启(操作可以省略)

# 测试练习
mysql> select * from t100w where k1='qj' and k2 like '%v%';
mysql> desc select * from t100w where k1='qj' and k2 like '%v%';
-- extra列显示using index condition信息,表示应用了索引下推

# 进行压测
mysqlslap --defaults-file=/etc/my.cnf --concurrency=100 --iterations=1 --create-schema='liux' --query="select * from t100w where k1='qj' and k2 like '%v%" engine=innodb --number-of-queries=20000 -uroot -p12366 -h10.0.0.51 -verbose
9.4 MRR

MRR,全称(Multi-Range Read Optimization 多范围读取操作);

在了解MRR概念之前,需要先掌握什么是回表概念;

回表是指,InnoDB在普通索引a上查到主键id的值后,再根据一个个主键id的值到主键索引上去查整行数据的过程。

由于聚簇索引是有回表的过程的,由于聚簇索引上引用的主键值不一定是有序的,因此就有可能造成大量的随机 IO;

如果回表前把主键值给它排一下序,那么在回表的时候就可以用顺序 IO 取代原本的随机 IO。

简单来说:MRR 通过把「随机磁盘读」,转化为「顺序磁盘读」,从而提高了索引查询的性能。

描述说明中涉及到的问题:

① 为什么要把随机读转换为顺序读? 减少磁盘压力,磁盘和磁头不再需要来回做机械运动;

② 为什么顺序读就能提升读取性能? 可以充分利用磁盘预读

③ 如何将随机读去转换为顺序读取? MRR(read_rnd_buffer-read_rnd_buffer_size)

知识点参考链接:https://blog.csdn.net/bookssea/article/details/126820604

sh 复制代码
MRR功能配置信息:
mysql > set optimizer_switch='mrr=on';
mysql > set global optimizer_switch='mrr_cost_based=off';
-- 用来告诉优化器,要不要基于使用 MRR 的成本,考虑使用 MRR 是否值得(cost-based choice)
Query OK, 0 rows affected (0.06 sec)

对于只返回一行数据的查询,是没有必要 MRR 的,而如果你把 mrr_cost_based 设为 off,那优化器就会通通使用 MRR,

这在有些情况下是很傻的,所以建议这个配置还是设为 on,毕竟优化器在绝大多数情况下都是正确的。

相关推荐
warton882 小时前
proxysql配置mysql mgr代理,实现读写分离
linux·运维·数据库·mysql
上天_去_做颗惺星 EVE_BLUE2 小时前
Android设备与Mac/Docker全连接指南:有线到无线的完整方案
android·linux·macos·adb·docker·容器·安卓
zhangphil2 小时前
Android显示系统性能分析:trace的HWUI All Memory与HWUI Misc Memory
android
2501_916008892 小时前
iOS开发APP上架全流程解析:从开发到App Store的完整指南
android·ios·小程序·https·uni-app·iphone·webview
better_liang2 小时前
Java技术栈中的MySQL数据结构应用与优化
java·数据结构·mysql·性能调优·索引优化
短剑重铸之日3 小时前
7天读懂MySQL|特别篇:MVCC详解
数据库·后端·mysql·mvcc
计算机毕设指导63 小时前
基于微信小程序技术校园拼车系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·maven
javachen__3 小时前
mysql系统级文件损坏修复
数据库·mysql
冬奇Lab3 小时前
【Kotlin系列08】泛型进阶:从型变到具体化类型参数的类型安全之旅
android·开发语言·windows·安全·kotlin