推荐链接:
总结——》【Java】
总结——》【Mysql】
总结——》【Redis】
总结——》【Kafka】
总结——》【Spring】
总结——》【SpringBoot】
总结——》【MyBatis、MyBatis-Plus】
总结——》【Linux】
总结——》【MongoDB】
总结——》【Elasticsearch】
MySQL执行一个查询可以有不同的执行方案,它会选择其中成本(代价)最低的方案去执行查询。
一个查询语句的执行成本 = I/O成本 + CPU成本。
MyISAM、InnoDB存储引擎都是将数据和索引都存储到磁盘上的,当我们想查询表中的记录时,需要先把数据或者索引加载到内存中然后再操作。
对于InnoDB存储引擎,页是磁盘和内存之间交互的基本单位。
从磁盘加载内存的过程损耗的时间称之为I/O成本。
读取以及检测记录是否满足搜索条件、对结果集进行排序等这些操作损耗的时间称之为CPU成本。
一个查询语句的执行其实是分为两层的:server层、存储引擎层,分别对应不同的常数表。
server层 | 存储引擎层 | |
---|---|---|
功能 | 进行连接管理、查询缓存、语法解析、查询优化等操作(与存储引擎无关) | 执行具体的数据存取操作 |
对应常数表 | mysql.server_cost | mysql.engine_cost |
常用成本常数 | row_evaluate_cost 功能:检测一条记录是否符合搜索条件的成本 默认:0.2 注意:不管读取记录时需不需要检测是否满足搜索条件,哪怕是空数据,其成本都算是0.2。 |
io_block_read_cost 功能:读取一个页面花费的成本 默认:1.0 |
-- 查看server对应的所有成本常数
SELECT * FROM mysql.server_cost;
-- 查看存储引擎对应的所有成本常数
SELECT * FROM mysql.engine_cost;
列 | 描述 |
---|---|
engine_name | 表示成本常数适用的存储引擎名称。如果值为default,表示适用于所有的存储引擎。 |
device_type | 表示存储引擎使用的设备类型,这主要是为了区分常规的机械硬盘和固态硬盘,不过在MySQL 5.7.X这个版本中并没有对机械硬盘的成本和固态硬盘的成本作区分,所以该值默认是0。 |
cost_name | 表示成本常数的名称 |
cost_value | 表示成本常数对应的值。如果值为NULL,表示采用默认值。 |
last_update | 表示最后更新记录的时间。 |
comment | 注释。 |
default_value | 表示成本常数对应的默认值。 |
成本常数 | 描述 |
---|---|
disk_temptable_create_cost | 创建基于磁盘的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。 |
disk_temptable_row_cost | 向基于磁盘的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。 |
key_compare_cost | 两条记录做比较操作的成本,多用在排序操作上,如果增大这个值的话会提升filesort的成本,让优化器可能更倾向于使用索引完成排序而不是filesort。 |
memory_temptable_create_cost | 创建基于内存的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。 |
memory_temptable_row_cost | 向基于内存的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。 |
row_evaluate_cost | 检测一条记录是否符合搜索条件的成本,增大这个值可能让优化器更倾向于使用索引而不是直接全表扫描。 |
Q:什么场景会创建临时表?
A:执行以下查询都可能在内部先创建一个临时表:
- DISTINCT查询
- 分组查询
- Union查询
- 某些特殊条件下的排序查询
数据量大:创建基于磁盘的临时表,使用MyISAM、InnoDB等存储引擎
数据量小:创建基于内存的临时表,使用Memory存储引擎
成本常数 | 描述 |
---|---|
io_block_read_cost | 从磁盘上读取一个块对应的成本。增大这个值会加重I/O成本,可能让优化器更倾向于选择使用索引执行查询而不是执行全表扫描。 |
memory_block_read_cost | 从内存中读取一个块对应的成本。 |
Q:不同存储引擎对块的定义不一样?
A:InnoDB:一个页 = 一个块
MyISAM:一个页 = 以4096字节作为一个块
Q:怎么从内存中和从磁盘上读取一个块的默认成本是一样的?
A:这主要是因为在MySQL目前的实现中,并不能准确预测某个查询需要访问的块中有哪些块已经加载到内存中,有哪些块还停留在磁盘上,所以MySQL简单的认为不管这个块有没有加载到内存中,使用的成本都是1.0。
-- 修改成本常数值,一定要让系统重新加载才生效
update mysql.server_cost set cost_value = 20 where cost_name = 'disk_temptable_create_cost';
-- 系统重新加载
FLUSH OPTIMIZER_COSTS;
-- 修改成本常数使用默认值,一定要让系统重新加载才生效
update mysql.server_cost set cost_value = null where cost_name = 'disk_temptable_create_cost'
-- 系统重新加载
FLUSH OPTIMIZER_COSTS;
在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的执行计划,之后才会调用存储引擎提供的接口真正的执行查询,这个过程总结一下就是这样:
- 根据搜索条件,找出所有可能使用的索引
- 计算全表扫描的代价
- 计算使用不同索引执行查询的代价
- 对比各种执行方案的代价,找出成本最低的那一个
MySQL把一个查询中可能使用到的索引称之为possible keys。在SQL查询的前面加上EXPLAIN关键字。
Q:可能用到索引的情况?
A:
情况1:索引列和常数使用=、<=>、IN、NOT IN、IS NULL、IS NOT NULL、>、<、>=、<=、BETWEEN、!=、<>
情况2:LIKE操作符(LIKE匹配字符串前缀也行)
Q:可能索引失效的情况?
A:
情况1:LIKE操作符,并且和以通配符开头的字符串做比较
情况2:如果列只有联合索引,并且不符合最左前缀原则
对于InnoDB存储引擎来说,全表扫描的过程:
1、首先把聚簇/主键索引对应的页面加载到内存中
2、然后检测记录是否符合搜索条件,并且把符合搜索条件的记录加入到结果集。
计算全表扫描的代价需要两个信息(参考链接:Mysql——》show table status):
1、聚簇索引占用的页面数
2、表中的记录数
I/O成本 = 30 * 1.0 +1.1 = 31.1
参数值 | 描述 |
---|---|
30 | 聚簇索引占用的页面数 |
1.0 | 加载一个页面的成本常数 |
1.1 | 微调值 |
Q:微调值?
A:_MySQL_在真实计算成本时会进行一些微调,这些微调的值是直接硬编码到代码里,没有注释而且这些微调的值十分的小,并不影响我们分析。
CPU成本 = 10093 x 0.2 + 1.0 = 2019.6
参数值 | 描述 |
---|---|
10093 | 统计数据中表的记录数 |
0.2 | 访问一条记录所需的成本常数 |
1.0 | 微调值 |
查询成本=I/O成本+CPU成本 = **31.1 + **2019.6 = 2050.7
所以,test全表扫描所需的总成本就是2050.7。
假设有可能使用索引idx_order_no、idx_expire_time,需要分别分析单独使用这些索引执行查询的成本,最后还要分析是否可能使用到索引合并。
如果是使用二级索引+回表,需要2个信息:
不论某个范围区间的二级索引到底占用了多少页面,查询优化器认为读取索引的一个范围区间的I/O成本和读取一个页面是相同的。
参考链接:Mysql——》index merge 索引合并
查看执行计划花费的成本的方式:EXPLAIN FORMAT = json 后再拼接真正的查询语句
EXPLAIN FORMAT = json select * from student where age >=3 and age <=10 and name like '%a%'
参数 | 描述 |
---|---|
query_cost | 查询总成本 |
cost_info | 成本明细 |
perfix_cost | 查询总成本 = read_cost + eval_cost |
参考链接:Mysql——》查询优化器
MySQL中连接查询采用嵌套循环连接算法,驱动表会被访问一次,被驱动表可能会被访问多次,所以两表连接的查询成本由下边两个部分构成:
- 单次查询驱动表的成本
- 多次查询被驱动表的成本(具体查询多少次取决于对驱动表查询的结果集中有多少条记录)
Q:扇出fanout?
A:对驱动表进行查询后得到的记录条数称之为驱动表的扇出。
驱动表的扇出值越小,对被驱动表的查询次数也就越少,连接查询的总成本也就越低。
当查询优化器想计算整个连接查询所使用的成本时,就需要计算出驱动表的扇出值,有的时候扇出值的计算是很容易的,很多时候在计算数据的时候很多时候只能靠猜。MySQL把这个猜的过程称之为 condition filtering 。当然,这个过程可能会使用到索引,也可能使用到统计数据,也可能就是MySQL单纯的瞎猜,整个评估过程非常复杂。
驱动表 | 扇出值 |
---|---|
使用全表扫描 | 表中记录的数量 |
使用索引 | 满足范围条件的索引记录条数 |
在MySQL 5.7版本中,引入condition filtering功能,就是还要猜一猜剩余的那些搜索条件能把驱动表中的记录再过滤多少条,其实本质上就是为了让成本估算更精确。我们所说的纯粹瞎猜其实是很不严谨的,MySQL称之为启发式规则。
连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本
特点:驱动表是固定的
最优的查询方案:只需要分别为驱动表和被驱动表选择成本最低的访问方法。
特点:驱动表和被驱动表的位置是可以互换的
需要考虑两个方面的问题:不同的表作为驱动表最终的查询成本可能是不同的,也就是需要考虑最优的表连接顺序。
最优的查询方案:分别为驱动表和被驱动表选择成本最低的访问方法。
多表连接 | 连接顺序 |
---|---|
两表 | 2× 1 = 2 |
三表 | 3 × 2 × 1 = 6 |
四表 | 4 × 3 × 2 × 1 = 24 |
n表 | n × (n-1) × (n-2) × ··· × 1 = n! = n的阶乘 |
n表进行连接,产生n!种连接顺序,MySQL查询优化器要每一种连接顺序的成本都计算一遍,不过MySQL用了很多办法减少计算非常多种连接顺序的成本的方法。
MySQL在计算各种链接顺序的成本之前,会维护一个全局的变量,这个变量表示当前最小的连接查询成本。如果在分析某个连接顺序的成本时,该成本已经超过当前最小的连接查询成本,那就压根儿不对该连接顺序继续往下分析了。
比方说A、B、C三个表进行连接,已经得到连接顺序ABC是当前的最小连接成本,比方说10.0,在计算连接顺序BCA时,发现B和C的连接成本就已经大于10.0时,就不再继续往后分析BCA这个连接顺序的成本了。
如果连接表的个数小于optimizer_search_depth值,那么就继续穷举分析每一种连接顺序的成本,否则只对与optimizer_search_depth值相同数量的表进行穷举分析。很显然,该值越大,成本分析的越精确,越容易得到好的执行计划,但是消耗的时间也就越长,否则得到不是很好的执行计划,但可以省掉很多分析连接成本的时间。
SHOW VARIABLES LIKE 'optimizer_search_depth';
即使是有上边两条规则的限制,但是分析多个表不同连接顺序成本花费的时间还是会很长,所以MySQL干脆提出了一些所谓的启发式规则(就是根据以往经验指定的一些规则),凡是不满足这些规则的连接顺序压根儿就不分析,这样可以极大的减少需要分析的连接顺序的数量,但是也可能造成错失最优的执行计划。他们提供了一个系统变量optimizer_prune_level来控制到底是不是用这些启发式规则。
SHOW VARIABLES LIKE 'optimizer_prune_level';