当前位置:   article > 正文

MySQL优化器如何预估查询成本_select * from order_exp as s1 inner join order_exp

select * from order_exp as s1 inner join order_exp2 as s2 on s1.order_no= s2

MySQL有哪些查询成本

MySQL 执行一个查询可以有不同的执行方案。在我们开发过程中,所有写过的sql语句都会丢给MySQL端的优化器。由优化器判断并选择其中成本最低,或者说代价最低的那种方案去真正的执行查询。

不过我们之前对成本的描述是非常模糊的,其实在 MySQL优化器去考察一条sql执行成本是由下边这两个方面。

I/O 成本

我们的表经常使用的 MyISAM、InnoDB 存储引擎都是将数据和索引都存储到磁盘上的,当我们想查询表中的记录时,需要先把数据或者索引加载到内存中然后再操作。这个从磁盘到内存这个加载的过程损耗的时间称之为 I/O 成本。

CPU 成本

读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为 CPU 成本。

对于 InnoDB 存储引擎来说,页是磁盘和内存之间交互的基本单位。

MySQL 规定读取一个页面花费的成本默认是 1.0,读取以及检测一条记录是否符合搜索条件的成本默认是 0.2。1.0、0.2 这些数字称之为成本常数,这两个成本常数我们最常用到,当然还有其他的成本常数。 注意,不管读取记录时需不需要检测是否满足搜索条件,其成本都算是 0.2。

MySQL底层在进行磁盘页筛查时,读取一个磁盘页的耗时是遍历一个磁盘页的5倍。应该是MySQL开发人员在做过大量测试后,得到的一个比较公正的数据。

单表查询的成本

优化器成本计算原理

在一条单表查询语句真正执行之前,MySQL 的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的执行计划,之后才会调用存储引擎提供的接口真正的执行查询,这个过程总结一下就是这样:

  1. 根据搜索条件,找出所有可能使用的索引
  2. 计算全表扫描的代价
  3. 计算使用不同索引执行查询的代价
  4. 对比各种执行方案的代价,找出成本最低的那一个

优化器成本计算实战

表索引:

image.png

查询语句:

  1. SELECT * FROM order_exp
  2. WHERE order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S')
  3. AND expire_time> '2021-03-22 18:28:28'
  4. AND expire_time<= '2021-03-22 18:35:09'
  5. AND insert_time> expire_time
  6. AND order_note LIKE '%7 排 1%'
  7. AND order_status = 0;

根据搜索条件,找出所有可能使用的索引

对于 B+树索引来说,只要索引列与常数使用=、<=>、IN、NOT IN、IS NULL、IS NOT NULL、>、<、>=、<=、BETWEEN、!=(不等于也可以写成<>)或者 LIKE 操作符连接起来,就可以产生一个所谓的范围区间(LIKE 匹配字符串前缀也行),MySQL 把一个查询中可能使用到的索引称之为 possible keys。

我们分析一下上边查询中涉及到的几个搜索条件:

  1. order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S') ,这个搜索条件可以使用二级索引 idx_order_no。
  2. expire_time> '2021-03-22 18:28:28' AND expire_time<= '2021-03-22 18:35:09',这个搜索条件可以使用二级索引 idx_expire_time。
  3. insert_time> expire_time,这个搜索条件的索引列由于没有和常数比较,所以并不能使用到索引。
  4. order_note LIKE '%hello%',order_note 即使有索引,但是通过 LIKE 操作符和以通配符开头的字符串做比较,不可以适用索引。order_status = 0,由于该列上只有联合索引,而且不符合最左前缀原则,所以不会用到索引。

综上所述,上边的查询语句可能用到的索引,也就是 possible keys 只有idx_order_no,idx_expire_time。

image.png

计算全面扫描的代价

对于 InnoDB 存储引擎来说,全表扫描的意思就是把聚簇索引中的记录都依 次和给定的搜索条件做一下比较,把符合搜索条件的记录加入到结果集,所以需 要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。由 于查询成本=I/O 成本+CPU 成本,所以计算全表扫描的代价需要两个信息:

聚簇索引占用的页面数

聚簇索引所在的B+树的所有叶子节点。

该表中的记录数

可以推倒出聚簇索引的CPU成本。

统计成本信息表

MySQL 为每个表维护了一系列的统计信息。提供了 SHOW TABLE STATUS 语句来查看表的统计信息,如果要看指定的某个表的统计信息,在该语句后加对应的 LIKE 语句就好了。

比方说我们要查看 order_exp 这个表的统计信息可以这么写:

SHOW TABLE STATUS LIKE 'order_exp'\G

image.png

出现了很多统计选项,但我们目前只需要两个:

Rows(估计行数)

本选项表示表中的记录条数。对于使用 MyISAM 存储引擎的表来说,该值是准确的,对于使用 InnoDB 存储引擎的表来说,该值是一个估计值。从查询结果我们也可以看出来,由于我们的 order_exp 表是使用 InnoDB 存储引擎的,所以 虽然实际上表中有 10567 条记录,但是 SHOW TABLE STATUS 显示的 Rows 值只有 10350 条记录。

Date_length(推倒页面数)

本选项表示表占用的存储空间字节数。使用 MyISAM 存储引擎的表来说,该值就是数据文件的大小,对于使用 InnoDB 存储引擎的表来说,该值就相当于聚簇索引占用的存储空间大小,也就是说可以这样计算该值的大小:

    Data_length = 聚簇索引的页面数量 x 每个页面的大小

我们的 order_exp 使用默认 16KB 的页面大小,而上边查询结果显示 Data_length 的值是 1589248,所以我们可以反向来推导出聚簇索引的页面数量:

聚簇索引的页面数量 = 1589248 ÷ 16 ÷ 1024 = 97

全表扫描成本计算

I/O成本

97 x 1.0 + 1.1 = 98.1 (1.0 指的是加载一个页面的成本常数,后边的 1.1 是一个微调值。)

TIPSMySQL 在真实计算成本时会进行一些微调,这些微调的值是直接硬编码到代码里的,没有注释而且这些微调的值十分的小,并不影响我们分析

CPU 成本

10350x 0.2 + 1.0 = 2071(0.2 指的是访问一条记录所需的成本常数,后边的 1.0 是一个微调值。)

成本

98.1 + 2071 = 2169.1

综上所述,对于 order_exp 的全表扫描所需的总成本就是 2169.1

TIPS:我们前边说过表中的记录其实都存储在聚簇索引对应 B+树的叶子节点 中,所以只要我们通过根节点获得了最左边的叶子节点,就可以沿着叶子节点组 成的双向链表把所有记录都查看一遍。

也就是说全表扫描这个过程其实有的 B+树非叶子节点是不需要访问的,但是 MySQL 在计算全表扫描成本时直接使用聚簇索引占用的页面数作为计算 I/O 成本的依据,是不区分非叶子节点和叶子节点的,全是叶子节点。

计算使用不同索引执行查询的代价

上述查询可能使用到 idx_order_no,idx_expire_time 这两个索引,我们需要分别分析单独使用这些索引执行查询的成本,最后还要分析是否可能使用到索引合并。

这里需要提一点的是,MySQL 查询优化器先分析使用唯一二级索引的成本,再分析使用普通索引的成本,我们这里两个索引都是普 通索引,先算哪个都可以。我们也先分析 idx_expire_time 的成本,然后再看使用idx_order_no 的成本。

范围查询成本分析

idx_expire_time对应的范围区间就是: ('2021-03-22 18:28:28' , '2021-03-22 18:35:09' )。

使用 idx_expire_time 搜索会使用用二级索引 + 回表方式的查询,MySQL计算这种查询的成本依赖两个方面的数据:

范围区间页面IO成本

不论某个范围区间的二级索引到底占用了多少页面,查询优化器认为读取索引的一个范围区间的I/O 成本和读取一个页面是相同的。

本例中使用 idx_expire_time 的范围区间只有一个,所以相当于访问这个范围区间的二级索引付出的 I/O 成本就是:1 x 1.0 = 1.0

需要回表的rows数(rows底层计算逻辑)

优化器计算二级索引计算('2021-03-22 18:28:28' ,'2021-03-22 18:35:09') 这个范围区间中包含多少二级索引记录的过程是这样的:

第一步:找最左索引B+树位置

使用语法:expire_time < ‘2021-03-22 18:28:28’找到满足这个条件的第一条记录。我们把这条记录称之为区间最左记录。我们前头说过在 B+数树中定位一条记录的过程是很快的,是常数级别的,所以这个过程的性能消 耗是可以忽略不计的。

第二步:找最由索引B+树位置

然后再根据 expire_time<= ‘2021-03-22 18:35:09’这个条件继续从 idx_expire_time 对应的 B+树索引中找出最后一条满足这个条件的记录,我们把 这条记录称之为区间最右记录,这个过程的性能消耗也可以忽略不计的。

第三步:求包含的索引数量

假设最左最右相隔不超过10个页面,则精准统计

假设最左最右超过10个页面,则从左向右取10个页面,计算每个页面数据量的平均值,然后再乘以最左数据到最有数据之间的页面数即可。

快速计算两个叶子节点中间相隔的页面数

那么如何统计最左到最右页面直接的页面数呢?

回想我们的B+树,实际上我们的B+树的父节点中的一项纪录,就会对应子节点中一整个数据页的。

因此,计算最左数据到最右数据之间存在多少数据页,只需要计算他们对应的父节点之间有多少项纪录即可快速统计。

使用 idx_expire_time 执行查询的成本计算

image.png

我们的rows其实在日常开发中是已经计算好了的。

第零步:范围查询IO成本

这个上面已经提到过了,为了方便计算总成本,再罗列一次。

1 x 1.0 = 1.0

范围二级索引扫描成本固定这个值。可见估计能用上磁盘顺序排列的特性。

第一步:范围读取二级索引的CPU成本(范围遍历一个值的cpu成本固定为0.2)

读取这 39 条二级索引记录需要付出的 CPU 成本就是:

39 x 0.2 + 0.01 = 7.81

其中 39 是需要读取的二级索引记录条数,0.2 是读取一条记录成本常数,0.01 是微调。

第二步:回表IO成本(这个包含了重新在聚餐索引查询的时间)

预计有 39 条二级索引需要进行回表操作,IO成本就是:

39 x 1.0 = 39 .0

其中 39 是预计的二级索引记录数,1.0 是一个页面的 I/O 成本常数。

第三步:在聚簇索引中再次排查其他条件CPU成本

读取并检测这些完整的用户记录是否符合其余的搜索条件的 CPU 成本如下:

39 x 0.2 =7.8

其中 39 是待检测记录的条数,0.2 是检测一条记录是否符合给定的搜索条件的成本常数。

第四步:计算idx_expire_time总耗时

IO总成本

1.0 + 39 x 1.0 = 40 .0 (范围区间的数量 + 预估的二级索引记录条数)

CPU总成本

39 x 0.2 + 0.01 + 39 x 0.2 = 15.61 (读取二级索引记录的成本 + 读取并检测 回表后聚簇索引记录的成本)

总成本

40 .0 + 15.61 = 55.61

使用 idx_order_no执行查询的成本计算

idx_order_no 对应的搜索条件是:order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S'),也就是说相当于 3 个单点区间。

与使用 idx_expire_time 的情况类似,我们也需要计算使用 idx_order_no 时需 要访问的范围区间数量以及需要回表的记录数,计算过程与上面类似,我们不详列所有计算步骤和说明了。

第零步:范围区间IO成本

使用 idx_order_no 执行查询时很显然有 3 个单点区间,所以访问这 3 个范围 区间的二级索引付出的 I/O 成本就是:

3 x 1.0 = 3.0

第一步:范围读取二级索引的CPU成本(遍历每个页)

58 x 0.2+0.01 = 11.61

第二步:回表IO成本(这个包含了重新在聚餐索引查询的时间

58 x 1.0 = 58.0

第三步:在聚簇索引中再次排查其他条件CPU成本

58 x 0.2 =7.8

第四步:计算idx_order_no总耗时

IO总成本

3.0 + 58 x 1.0 = 61.0 (范围区间的数量 + 预估的二级索引记录条数)

CPU总成本

58 x 0.2 + 0.01 + 58 x 0.2 = 15.61 (读取二级索引记录的成本 + 读取并检测回表后聚簇索引记录的成本)

总成本

61.0 + 23.21 = 84.21

是否有可能使用索引合并

本例中有关 order_no 和 expire_time 的搜索条件是使用 AND 连接起来的,而 对于 idx_order_no 和 idx_expire_time 都是范围查询,也就是说查找到的二级索引 记录并不是按照主键值进行排序的,并不满足使用 Intersection 索引合并的条件, 所以并不会使用索引合并。而且 MySQL 查询优化器计算索引合并成本的算法也比较麻烦,所以我们也不会细说

对比两条索引的总时间

下边把执行本例中的查询的各种可执行方案以及它们对应的成本列出来:

  • 全表扫描的成本:2148.7
  • 使用 idx_expire_time 的成本:55.61
  • 使用 idx_order_no 的成本:84.21

很显然,使用 idx_expire_time 的成本最低,所以当然选择 idx_expire_time 来执行查询。

image.png

注意

  1. MySQL 的源码中对成本的计算实际要更复杂
  2. 在 MySQL 的实际计算中,在和全文扫描比较成本时,使用索引的成本会去除读取并检测回表后聚簇索引记录的成本,也就是说,我们通过 MySQL 看到的成本将会是:idx_expire_time 为 47.81(55.61-7.8),idx_order_no 为72.61(84.21-11.6)。但是 MySQL 比较完成本后,会再计算一次使用索引的成本,此时就会加上去除读取并检测回表后聚簇索引记录的成本,也就是我们计算出来的值

IN语句的特殊优化

index dive(较少小区间计算成本方式)

我们看下面这个查询

SELECT * FROM order_exp WHERE order_no IN ('aa1', 'aa2', 'aa3', ... , 'zzz');

order_no并不是唯一二级索引。因此,这个查询就会可能会产生非常多的叶子节点小区间。

假设这种小的区间不是特别多的话,MySQL会采取对每一个区间获取索引对应的 B+树的区间最左记录和区间最右记录,然后再计算这两条记录之间有多少记录(记录条数少的时候可以做到精确计算,多的时候只能估算)。

而这种挨个计算范围区间的方式,我们称之为index dive。

若是仅仅有零星几个区间的话还好,如果 IN 语句里 20000 个参数怎么办?

较多小区间成本预估(参数eq_range_index_dive_limit)

假设要计算 20000 次 index dive 操作,这性能损耗就很大,搞不好计算这些单点 区间对应的索引记录条数的成本比直接全表扫描的成本都大了。MySQL 考虑到了 这种情况,所以提供了一个:eq_range_index_dive_limit系统变量来解决这个问题。

show variables like '%dive%';

也就是说如果我们的 IN 语句中的参数个数小于 200 个的话,将使用 index dive 的方式计算各个单点区间对应的记录条数,如果大于或等于 200 个的话,可 就不能使用 index dive 了,要使用所谓的索引统计数据来进行估算。怎么个估算法?

我们来看一下mysql提供的索引信息

image.png

这里我们主要介绍Cardinality这个列。

Cardinality 直译过来就是基数的意思,表示索引列中不重复值的个数。比如对于一个一万行记录的表来说,某个索引列的 Cardinality 属性 是 10000,那意味着该列中没有重复的值,如果 Cardinality 属性是 1 的话,就意味着该列的值全部是重复的。不过需要注意的是,对于 InnoDB 存储引擎来说, 使用 SHOW INDEX 语句展示出来的某个索引列的 Cardinality 属性是一个估计值, 并不是精确的。你问我怎么预估的?这个我也没研究,但估计和rows的计算方式也很像。

结合我们explain中得到的 Rows 统计数据,我们可以针对索引列,计算出平均一个值重复多少次。

一个值的重复次数 ≈ Rows ÷ Cardinality。

以 order_exp 表的 idx_order_no 索引为例,它的 Rows 值是 10350,它对应的 Cardinality 值是 10220,所以我们可以计算 order_no 列平均单个值的重复次数 就是:10350÷ 10220≈ 1.012(条)。

假设 IN 语句中有 20000 个参数的话,就直接使用统计数据来估算这些参数 需要单点区间对应的记录条数了,每个参数大约对应 1.012 条记录,所以总共需 要回表的记录数就是:

20000 x 1.012= 21,730

使用统计数据来计算IN语句记录条数比 index dive 的方式简单, 但是它的致命弱点就是:不精确!。使用统计数据算出来的查询成本与实际所需的成本可能相差非常大。

在 MySQL 5.7.3 以及之前的版本中, eq_range_index_dive_limit 的默认值为 10,之后的版本默认值为 200。随着阈值变大,在精准度与计算速度之间做出了权衡。

EXPLAIN 输出成本

EXPLAIN 语法可以说非常好用了,给我们提供了许多分析sql的信息。但EXPLAIN 语句输出中缺少了一个衡量执行计划好坏的重要属性。

在 EXPLAIN 单词和真正的查询语句中间加上 FORMAT=JSON。 这样我们就可以得到一个 json 格式的执行计划,里边包含该计划花费的成 本,比如这样:

  1. explain format=json SELECT * FROM order_exp
  2. WHERE order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S')
  3. AND expire_time> '2021-03-22 18:28:28'
  4. AND expire_time<= '2021-03-22 18:35:09'
  5. AND insert_time> expire_time
  6. AND order_note LIKE '%7 排 1%'
  7. AND order_status = 0;

image.png

Optimizer Trace优化器执行成本跟踪

在 MySQL 5.6 以及之后的版本中,MySQL 提出了一个 optimizer trace 的功能, 这个功能可以让我们方便的查看优化器生成执行计划的整个过程,这个功能的开 启与关闭由系统变量 optimizer_trace 决定:

SHOW VARIABLES LIKE 'optimizer_trace';

image.png

可以看到 enabled 值为 off,表明这个功能默认是关闭的。

开启optimizer_trace

SET optimizer_trace="enabled=on";

image.png

然后我们就可以输入我们想要查看优化过程的查询语句,当该查询语句执行 完成后,就可以到 information_schema 数据库下的 OPTIMIZER_TRACE 表中查看完 整的优化过程。这个 OPTIMIZER_TRACE 表有 4 个列,分别是:

  1. QUERY:表示我们的查询语句。
  2. TRACE:表示优化过程的 JSON 格式文本。
  3. MISSING_BYTES_BEYOND_MAX_MEM_SIZE:由于优化过程可能会输出很多, 如果超过某个限制时,多余的文本将不会被显示,这个字段展示了被忽略的文本 字节数。I
  4. NSUFFICIENT_PRIVILEGES:表示是否没有权限查看优化过程,默认值是 0, 只有某些特殊情况下才会是 1,我们暂时不关心这个字段的值。

注意:开启 trace 会影响 mysql 性能,所以只能临时分析 sql 使用,用完之后立即关闭 。

连接查询成本

Condition filtering 介绍

我们前边说过,MySQL 中连接查询采用的是嵌套循环连接算法,驱动表会被访问一次,被驱动表可能会被访问多次,所以对于两表连接查询来说,它的查询成本由下边两个部分构成:

  1. 单次查询驱动表的成本
  2. 多次查询被驱动表的成本(具体查询多少次取决于对驱动表查询的结果集 中有多少条记录)

对驱动表进行查询后得到的记录条数称之为驱动表的扇出(英文名:fanout)。 很显然驱动表的扇出值越小,对被驱动表的查询次数也就越少,连接查询的总成本也就越低。当查询优化器想计算整个连接查询所使用的成本时,就需要计算出 驱动表的扇出值,有的时候扇出值的计算是很容易的,比如下边查询:

索引一览

image.png

查询一:

SELECT * FROM order_exp AS s1 INNER JOIN order_exp2 AS s2;

假设使用 s1 表作为驱动表,很显然对驱动表的单表查询只能使用全表扫描 的方式执行,驱动表的扇出值也很明确,那就是驱动表中有多少记录,扇出值就是多少。统计数据中s1表的记录行数是10573,也就是说优化器就直接会把10573 当作在 s1 表的扇出值。

查询二:

  1. SELECT * FROM order_exp AS s1 INNER JOIN order_exp2 AS s2
  2. WHERE s1.expire_time> '2021-03-22 18:28:28'
  3. AND s1.expire_time<= '2021-03-22 18:35:09';

仍然假设 s1 表是驱动表的话,很显然对驱动表的单表查询可以使用 idx_expire_time 索引执行查询。此时范围区间( '2021-03-22 18:28:28', '2021-03-22 18:35:09')中有多少条记录,那么扇出值就是多少。

但是有的时候扇出值的计算就变得很棘手,比方说下边几个查询:

查询三:

SELECT * FROM order_exp AS s1 INNER JOIN order_exp2 AS s2 WHERE s1.order_note > 'xyz';

可以看到order_note压根儿不是索引。所以它只能猜这 10573 记录里有 多少条记录满足 order_note > 'xyz'条件。

查询四:

  1. SELECT * FROM order_exp AS s1 INNER JOIN order_exp2 AS s2
  2. WHERE s1.expire_time> '2021-03-22 18:28:28'
  3. AND s1.expire_time<= '2021-03-22 18:35:09'
  4. AND s1.order_note > 'xyz';

本查询可以使用 idx_expire_time 索引,所以只需要从符合 二级索引范围区间的记录中猜有多少条记录符合 order_note > 'xyz'条件,也就是 只需要猜在 39 条记录中有多少符合 order_note > 'xyz'条件。

image.png

查询五:

  1. SELECT * FROM order_exp AS s1 INNER JOIN order_exp2 AS s2
  2. WHERE s1.expire_time> '2021-03-22 18:28:28'
  3. AND s1.expire_time<= '2021-03-22 18:35:09'
  4. AND s1.order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S')
  5. AND s1.order_note > 'xyz';

s1 选取 idx_expire_time 索引执行查询 后,优化器需要从符合二级索引范围区间的记录中猜有多少条记录符合下边两个条件:

order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S')

order_note > 'xyz

扇出时什么时候需要猜满足条件的记录(condition filtering

也就是优化器需要猜在 39 条记录中有多少符合上述两个条件的。 说了这么多,其实就是想表达在这两种情况下计算驱动表扇出值时需要靠猜:

  1. 如果使用的是全表扫描的方式执行的单表查询,那么计算驱动表扇出时需 要猜满足搜索条件的记录到底有多少条。
  2. 如果使用的是索引执行的单表扫描,那么计算驱动表扇出的时候需要猜满 足除使用到对应索引的搜索条件外的其他搜索条件的记录有多少条。

MySQL 把这个猜的过程称之为 condition filtering。当然,这个过程可能会使用到索引,也可能使用到统计数据,也可能就是 MySQL 单纯的瞎猜,整个评估过程非常复杂,所以我们不去细讲。

在 MySQL 5.7 之前的版本中,查询优化器在计算驱动表扇出时,如果是使用全表扫描的话,就直接使用表中记录的数量作为扇出值,如果使用索引的话,就 直接使用满足范围条件的索引记录条数作为扇出值。

在 MySQL 5.7 中,MySQL 引入了这个 condition filtering 的功能,就是还要猜 一猜剩余的那些搜索条件能把驱动表中的记录再过滤多少条,其实本质上就是为 了让成本估算更精确。 我们所说的纯粹瞎猜其实是很不严谨的,MySQL 称之为 启发式规则。

两表连接的成本分析

连接查询的成本计算公式是这样的:

连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数 x 单次访问被驱动表的成本

对于左(外)连接和右(外)连接查询来说,它们的驱动表是固定的,所以想要得到最优的查询方案必须要我们人为来分别为驱动表和被驱动表选择成本最低的访问方法。

可是对于内连接来说,驱动表和被驱动表的位置是可以互换的,所以需要考虑两个方面的问题: 不同的表作为驱动表最终的查询成本可能是不同的,也就是需要考虑最优的表连接顺序。然后分别为驱动表和被驱动表选择成本最低的访问方法。

很显然,计算内连接查询成本的方式更麻烦一些,由优化器来选择驱动表。

内连接选择驱动表实战

我们来看看内连接,比如对于下边这个查询来说:

image.png

image.png

  1. SELECT * FROM order_exp AS s1
  2. INNER JOIN order_exp2 AS s2 ON s1.order_no= s2.order_note
  3. WHERE s1.expire_time> '2021-03-22 18:28:28'
  4. AND s1.expire_time<= '2021-03-22 18:35:09'
  5. AND s2.expire_time> '2021-03-22 18:35:09'
  6. AND s2.expire_time<= '2021-03-22 18:35:59';

可以选择的连接顺序有两种:

s1 连接 s2,也就是 s1 作为驱动表,s2 作为被驱动表。

s2 连接 s1,也就是 s2 作为驱动表,s1 作为被驱动表。

查询优化器需要分别考虑这两种情况下的最优查询成本,然后选取那个成本 更低的连接顺序以及该连接顺序下各个表的最优访问方法作为最终的查询计划。 我们定性的分析一下,不像分析单表查询那样定量(与常量关联)的分析了:

使用 s1 作为驱动表的情况

分析对于驱动表的成本最低的执行方案,首先看一下涉及 s1 表单表的搜索条件有哪些:

s1.expire_time> '2021-03-22 18:28:28' AND s1.expire_time<= '2021-03-22 18:35:09'

很显然会使用 idx_expire_time这条索引。

然后分析对于被驱动表的成本最低的执行方案,此时涉及被驱动表 s2 的搜索条件就是:

  1. s2.order_note = 常数(这是因为对驱动表 s1 结果集中的每一条记录,都 需要进行一次被驱动表 s2 的访问,此时那些涉及两表的条件现在相当于只涉及 被驱动表 s2 了。)
  2. s2.expire_time> '2021-03-22 18:35:09' AND s2.expire_time<= '2021-03-22 18:35:59'

第一个条件由于 order_note 没有用到索引,所以并没有什么用, 此时访问 s2 表时可用的方案也是全表扫描和使用 idx_expire_time 两种,假设使 用 idx_expire_time 的成本更小。

所以此时使用 s1 作为驱动表时的总成本就是(暂时不考虑使用 join buffer 对成本的影响):

使用 idx_expire_time 访问 s1 的成本 + s1 的扇出 × 使用 idx_expire_time 访问 s2 的成本

使用 s2 作为驱动表的情况

分析对于驱动表的成本最低的执行方案

s2.expire_time> '2021-03-22 18:35:09' AND s2.expire_time<= '2021-03-22 18:35:59

很显然也会使用 idx_expire_time这条索引。

然后分析对于被驱动表的成本最低的执行方案,此时涉及被驱动表 s1的搜索条件就是:

  1. s1.order_no = 常数
  2. s1.expire_time> '2021-03-22 18:28:28' AND s1.expire_time<= '2021-03-22 18:35:09

与s1作为驱动表的区别在于,s1.order_no = 常数这个条件是可以使用索引的。

因此优化器有了三种扫描方案

  1. 全表扫描
  2. 使用 idx_order_no 可以进行 ref 方式的访问
  3. 使用 idx_expire_time 可以使用 range 方式的访问。

因为 idx_expire_time 的范围区间是确定的,怎么计算使用 idx_expire_time 的成本我们上边已经说过了。

问题在于没有真正执行查询前, s1.order_no = 常数中的常数值我们是不知道的,怎么衡量使用 idx_order_no 执 行查询的成本呢?

其实很简单,直接使用我们前面说过的索引统计数据就好了 (就是索引列平均一个值重复多少次)。一般情况下,ref 的访问方式要比 range 成本更低。

连接查询优化总结

综上所述,我们的优化重点其实是下边这两个部分:

尽量减少驱动表的扇出 (选择对的驱动表)

对被驱动表的访问成本尽量低 (为被驱动表添加索引)

这一点对于我们实际书写连接查询语句时十分有用,我们需要尽量在被驱动表的连接列上建立索引,这样就可以使用 ref 访问方法来降低访问被驱动表的成 本了。如果可以,被驱动表的连接列最好是该表的主键或者唯一二级索引列,这样就可以把访问被驱动表的成本降到更低了。

多表连接的成本分析

表与表之间的组合关系数

首先要考虑一下多表连接时可能产生出多少种连接顺序:

对于两表连接,比如表 A 和表 B 连接,只有 AB、BA 这两种连接顺序。其实相当于 2 × 1 = 2 种连接顺序。

对于三表连接,比如表 A、表 B、表 C 进行连接 有 ABC、ACB、BAC、BCA、CAB、CBA 这么 6 种连接顺序。其实相当于 3 × 2 × 1 = 6 种连接顺序。

对于四表连接的话,则会有 4 × 3 × 2 × 1 = 24 种连接顺序。 对于 n 表连接的话,则有 n × (n-1) × (n-2) × ··· × 1 种连接顺序, 就是 n 的阶乘种连接顺序,也就是 n!。

有 n 个表进行连接,MySQL 查询优化器要每一种连接顺序的成本都计算一 遍么?那可是 n!种连接顺序呀。其实真的是要都算一遍,不过 MySQL 用了很多办法减少计算非常多种连接顺序的成本的方法:

减少顺序连接成本的途径

提前结束某种顺序的成本评估

MySQL 在计算各种链接顺序的成本之前,会维护一个全局的变量,这个变量 表示当前最小的连接查询成本。如果在分析某个连接顺序的成本时,该成本已经 超过当前最小的连接查询成本,那就压根儿不对该连接顺序继续往下分析了。比 方说 A、B、C 三个表进行连接,已经得到连接顺序 ABC 是当前的最小连接成本,比方说 10.0,在计算连接顺序 BCA 时,发现 B 和 C 的连接成本就已经大于 10.0 时,就不再继续往后分析 BCA 这个连接顺序的成本了。

系统变量 optimizer_search_depth(超多表连接情况下)

为了防止无穷无尽的分析各种连接顺序的成本,MySQL 提出了 optimizer_search_depth 系统变量,如果连接表的个数小于该值,那么就继续穷举分析每一种连接顺序的成本,否则只对与 optimizer_search_depth 值相同数量 的表进行穷举分析。

很显然,该值越大,成本分析的越精确的执行计划,但是消耗的时间也就越长。因此,这是一个在筛选计划,与执行连接速度之间进行的一种取舍。

调节成本常数

我们前边已经介绍了两个成本常数:

读取一个页面花费的成本默认是 1.0

检测一条记录是否符合搜索条件的成本默认是 0.2

其实除了这两个成本常数,MySQL 还支持很多,它们被存储到了 MySQL 数 据库的两个表中:

SHOW TABLES FROM mysql LIKE '%cost%';

image.png

因为一条语句的执行其实是分为两层的:server 层、存储引擎层。 在 server 层进行连接管理、查询缓存、语法解析、查询优化等操作,在存储 引擎层执行具体的数据存取操作。也就是说一条语句在 server 层中执行的成本是 和它操作的表使用的存储引擎是没关系的,所以关于这些操作对应的成本常数就 存储在了 server_cost 表中,而依赖于存储引擎的一些操作对应的成本常数就存 储在了 engine_cost 表中。

mysql.server_cost 表

server_cost 表中在 server 层进行的一些操作对应的成本常数,具体内容如下:

SELECT * FROM mysql.server_cost;

image.png

列名介绍

cost_name

表示成本常数的名称。

cost_value

表示成本常数对应的值。如果该列的值为 NULL 的话,意味着对应的成本常数会采用默认值。

last_update

表示最后更新记录的时间。

comment

注释

行名介绍

disk_temptable_create_cost

默认值 40.0 创建基于磁盘的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。

disk_temptable_row_cost

默认值 1.0 向基于磁盘的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于磁盘的临时表。

key_compare_cost

默认值 0.1 两条记录做比较操作的成本,多用在排序操作上,如果增大这个值的话会提升filesort的成本,让优化器可能更倾向于使用索引完成排序而不是filesort。

memory_temptable_creat

默认值 0.1 两条记录做比较操作的成本,多用在排序操作上,如果增大这个值的话会提升filesort的成本,让优化器可能更倾向于使用索引完成排序而不是filesort。

memory_temptable_row_cost

默认值 2.0 创建基于内存的临时表的成本, 如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。

row_evaluate_cost

默认值 0.2 检测一条记录是否符合搜索条件的成本,增大这个值可能让优化器更倾向于使用索引而不是直接全表扫描。

内存临时表和磁盘临时表

MySQL 在执行诸如 DISTINCT 查询、分组查询、Union 查询以及某些特殊条件 下的排序查询都可能在内部先创建一个临时表,使用这个临时表来辅助完成查询 (比如对于 DISTINCT 查询可以建一个带有 UNIQUE 索引的临时表,直接把需要去 重的记录插入到这个临时表中,插入完成之后的记录就是结果集了)。

在数据量 大的情况下可能创建基于磁盘的临时表,也就是为该临时表使用MyISAM、InnoDB等存储引擎,在数据量不大时可能创建基于内存的临时表,也就是使用 Memory 存储引擎。大家可以看到,创建临时表和对这个临时表进行写入和读取的操作代价还是很高的就行了

修改参数mysql.server_cost 表参数

这些成本常数在 server_cost 中的初始值都是 NULL,意味着优化器会使用它 们的默认值来计算某个操作的成本,如果我们想修改某个成本常数的值的话,需要做两个步骤:

对我们感兴趣的成本常数做 update 更新操作,然后使用下边语句即可: FLUSH OPTIMIZER_COSTS; (不执行这句话不会生效)。

当然,在你修改完某个成本常数后想把它们再改回默认值的话,可以直接把 cost_value 的值设置为 NULL,再使用 FLUSH OPTIMIZER_COSTS(为了生效而执行) 语句让系统重新加载。

mysql.engine_cost 表

engine_cost 表表中在存储引擎层进行的一些操作对应的成本常数,具体内容如下:

SELECT * FROM mysql.engine_cost;

列名介绍

与 server_cost 相比,engine_cost 多了两个列

engine_name

指成本常数适用的存储引擎名称。如果该值为 default,意味着对应的成本常数适用于所有的存储引擎

device_type

指存储引擎使用的设备类型,这主要是为了区分常规的机械硬盘和固态硬盘, 不过在 MySQL 5.7.X 这个版本中并没有对机械硬盘的成本和固态硬盘的成本作区分,所以该值默认是 0

行名介绍

io_block_read_cost

默认值 1.0 从磁盘上读取一个块对应的成本。请注意我使用的是块,而不是页这个词。对于 InnoDB 存储引擎来说,一个页就是一个块, 不过对于 MyISAM 存储引擎来说,默认是以 4096 字节作为一个块的。增大这个值会加重 I/O 成本,可能让优化器更倾向于选择使用索引执行查询而不是执行全表扫描。

memory_block_read_cost

默认值 1.0 与上一个参数类似,只不过衡量的是从内存中读取一个块对应的成本。

怎么从内存中和从磁盘上读取一个块的默认成本是一样的?

这主要是因为 在 MySQL 目前的实现中,并不能准确预测某个查询需要访问的块中有哪些块已经加载到内存中,有哪些块还停留在磁盘上,所以 MySQL 简单的认为不管这个块有没有加载到内存中,使用的成本都是 1.0。

与更新 server_cost 表中的记录一样,我们也可以通过更新 engine_cost 表中 的记录来更改关于存储引擎的成本常数,做法一样。

修改参数mysql.engine_cost 表参数

与更新 server_cost 表中的记录一样,我们也可以通过更新 engine_cost 表中 的记录来更改关于存储引擎的成本常数,做法一样。

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/529066
推荐阅读
相关标签
  

闽ICP备14008679号