本文系统讲解了数据库执行计划的核心概念、获取方法、关键指标解读及索引优化策略。详细阐述了执行计划中各字段的含义,重点分析了索引应用类型(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,毕竟优化器在绝大多数情况下都是正确的。