CategoryResourceRepost/极客时间专栏/SQL必知必会/第二章:SQL性能优化篇/32丨查询优化器是如何工作的?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

142 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="32丨查询优化器是如何工作的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0e/e0/0ee4889b72141f4094d9ddcb104a15e0.mp3"></audio>
我们总是希望数据库可以运行得更快,也就是响应时间更快,吞吐量更大。想要达到这样的目的,我们一方面需要高并发的事务处理能力,另一方面需要创建合适的索引,让数据的查找效率最大化。事务和索引的使用是数据库中的两个重要核心,事务可以让数据库在增删查改的过程中,保证数据的正确性和安全性,而索引可以帮数据库提升数据的查找效率。
如果我们想要知道如何获取更高的SQL查询性能最好的方式就是理解数据库是如何进行查询优化和执行的。
今天我们就来看看查询优化的原理是怎么一回事。今天的主要内容包括以下几个部分:
1. 什么是查询优化器一条SQL语句的执行流程都会经历哪些环节在查询优化器中都包括了哪些部分
1. 查询优化器的两种优化方式分别是什么?
1. 基于代价的优化器是如何统计代价的?总的代价又如何计算?
## 什么是查询优化器
了解查询优化器的作用之前我们先来看看一条SQL语句的执行都需要经历哪些环节如下图所示
<img src="https://static001.geekbang.org/resource/image/67/31/6776cd76ea50db263bfd9d58c4d98631.png" alt=""><br>
你能看到一条SQL查询语句首先会经过分析器进行语法分析和语义检查。我们之前讲过语法分析是检查SQL拼写和语法是否正确语义检查是检查SQL中的访问对象是否存在。比如我们在写SELECT语句的时候列名写错了系统就会提示错误。语法检查和语义检查可以保证SQL语句没有错误最终得到一棵语法分析树然后经过查询优化器得到查询计划最后交给执行器进行执行。
查询优化器的目标是找到执行SQL查询的最佳执行计划执行计划就是查询树它由一系列物理操作符组成这些操作符按照一定的运算关系组成查询的执行计划。在查询优化器中可以分为逻辑查询优化阶段和物理查询优化阶段。
逻辑查询优化就是通过改变SQL语句的内容来使得SQL查询更高效同时为物理查询优化提供更多的候选执行计划。通常采用的方式是对SQL语句进行等价变换对查询进行重写而查询重写的数学基础就是关系代数。对条件表达式进行等价谓词重写、条件简化对视图进行重写对子查询进行优化对连接语义进行了外连接消除、嵌套连接消除等。
逻辑查询优化是基于关系代数进行的查询重写,而关系代数的每一步都对应着物理计算,这些物理计算往往存在多种算法,因此需要计算各种物理路径的代价,从中选择代价最小的作为执行计划。在这个阶段里,对于单表和多表连接的操作,需要高效地使用索引,提升查询效率。
<img src="https://static001.geekbang.org/resource/image/65/fa/650cd690c532161de1b0e856d4b067fa.png" alt=""><br>
在这两个阶段中,查询重写属于代数级、语法级的优化,也就是属于逻辑范围内的优化,而基于代价的估算模型是从连接路径中选择代价最小的路径,属于物理层面的优化。
## 查询优化器的两种优化方式
查询优化器的目的就是生成最佳的执行计划,而生成最佳执行计划的策略通常有以下两种方式。
第一种是基于规则的优化器RBORule-Based Optimizer规则就是人们以往的经验或者是采用已经被证明是有效的方式。通过在优化器里面嵌入规则来判断SQL查询符合哪种规则就按照相应的规则来制定执行计划同时采用启发式规则去掉明显不好的存取路径。
第二种是基于代价的优化器CBOCost-Based Optimizer这里会根据代价评估模型计算每条可能的执行计划的代价也就是COST从中选择代价最小的作为执行计划。相比于RBO来说CBO对数据更敏感因为它会利用数据表中的统计信息来做判断针对不同的数据表查询得到的执行计划可能是不同的因此制定出来的执行计划也更符合数据表的实际情况。
但我们需要记住SQL是面向集合的语言并没有指定执行的方式因此在优化器中会存在各种组合的可能。我们需要通过优化器来制定数据表的扫描方式、连接方式以及连接顺序从而得到最佳的SQL执行计划。
你能看出来RBO的方式更像是一个出租车老司机凭借自己的经验来选择从A到B的路径。而CBO更像是手机导航通过数据驱动来选择最佳的执行路径。
## CBO是如何统计代价的
大部分RDBMS都支持基于代价的优化器CBOCBO随着版本的迭代也越来越成熟但是CBO依然存在缺陷。通过对CBO工作原理的了解我们可以知道CBO可能存在的不足有哪些有助于让我们知道优化器是如何确定执行计划的。
### 能调整的代价模型的参数有哪些
首先我们先来了解下MySQL中的`COST Model``COST Model`就是优化器用来统计各种步骤的代价模型在5.7.10版本之后MySQL会引入两张数据表里面规定了各种步骤预估的代价Cost Value ,我们可以从`mysql.server_cost`和`mysql.engine_cost`这两张表中获得这些步骤的代价:
```
SQL &gt; SELECT * FROM mysql.server_cost
```
<img src="https://static001.geekbang.org/resource/image/d5/e7/d571ea13f753ed24eedf6f20183e2ae7.png" alt=""><br>
server_cost数据表是在server层统计的代价具体的参数含义如下
1. `disk_temptable_create_cost`表示临时表文件MyISAM或InnoDB的创建代价默认值为20。
1. `disk_temptable_row_cost`表示临时表文件MyISAM或InnoDB的行代价默认值0.5。
1. `key_compare_cost`表示键比较的代价。键比较的次数越多这项的代价就越大这是一个重要的指标默认值0.05。
1. `memory_temptable_create_cost`表示内存中临时表的创建代价默认值1。
1. `memory_temptable_row_cost`表示内存中临时表的行代价默认值0.1。
1. `row_evaluate_cost`统计符合条件的行代价如果符合条件的行数越多那么这一项的代价就越大因此这是个重要的指标默认值0.1。
由这张表中可以看到,如果想要创建临时表,尤其是在磁盘中创建相应的文件,代价还是很高的。
然后我们看下在存储引擎层都包括了哪些代价:
```
SQL &gt; SELECT * FROM mysql.engine_cost
```
<img src="https://static001.geekbang.org/resource/image/96/72/969377b6a1348175ce211f137e2cf572.png" alt=""><br>
`engine_cost`主要统计了页加载的代价我们之前了解到一个页的加载根据页所在位置的不同读取的位置也不同可以从磁盘I/O中获取也可以从内存中读取。因此在`engine_cost`数据表中对这两个读取的代价进行了定义:
1. `io_block_read_cost`从磁盘中读取一页数据的代价默认是1。
1. `memory_block_read_cost`从内存中读取一页数据的代价默认是0.25。
既然MySQL将这些代价参数以数据表的形式呈现给了我们我们就可以根据实际情况去修改这些参数。因为随着硬件的提升各种硬件的性能对比也可能发生变化比如针对普通硬盘的情况可以考虑适当增加`io_block_read_cost`的数值,这样就代表从磁盘上读取一页数据的成本变高了。当我们执行全表扫描的时候,相比于范围查询,成本也会增加很多。
比如我想将`io_block_read_cost`参数设置为2.0,那么使用下面这条命令就可以:
```
UPDATE mysql.engine_cost
SET cost_value = 2.0
WHERE cost_name = 'io_block_read_cost';
FLUSH OPTIMIZER_COSTS;
```
<img src="https://static001.geekbang.org/resource/image/d5/96/d5e43099f925fc5d271055d69c0d6996.png" alt=""><br>
我们对`mysql.engine_cost`中的`io_block_read_cost`参数进行了修改,然后使用`FLUSH OPTIMIZER_COSTS`更新内存,然后再查看`engine_cost`数据表,发现`io_block_read_cost`参数中的`cost_value`已经调整为2.0。
如果我们想要专门针对某个存储引擎比如InnoDB存储引擎设置`io_block_read_cost`比如设置为2可以这样使用
```
INSERT INTO mysql.engine_cost(engine_name, device_type, cost_name, cost_value, last_update, comment)
VALUES ('InnoDB', 0, 'io_block_read_cost', 2,
CURRENT_TIMESTAMP, 'Using a slower disk for InnoDB');
FLUSH OPTIMIZER_COSTS;
```
然后我们再查看一下`mysql.engine_cost`数据表:
<img src="https://static001.geekbang.org/resource/image/4d/75/4def533e214f9a99f39d6ad62cc2b775.png" alt=""><br>
从图中你能看到针对InnoDB存储引擎可以设置专门的`io_block_read_cost`参数值。
### 代价模型如何计算
总代价的计算是一个比较复杂的过程,上面只是列出了一些常用的重要参数,我们可以根据情况对它们进行调整,也可以使用默认的系统参数值。
那么总的代价是如何进行计算的呢?
在论文[《Access Path Selection-in a Relational Database Management System》](http://dbis.rwth-aachen.de/lehrstuhl/staff/li/resources/download/AccessPathSelectionInRelationalDatabase.pdf%EF%BC%89)中给出了计算模型,如下图所示:
<img src="https://static001.geekbang.org/resource/image/38/b0/387aaaeb22f50168402018d8891ca5b0.png" alt=""><br>
你可以简单地认为总的执行代价等于I/O代价+CPU代价。在这里PAGE FETCH就是I/O代价也就是页面加载的代价包括数据页和索引页加载的代价。W*(RSI CALLS)就是CPU代价。W在这里是个权重因子表示了CPU到I/O之间转化的相关系数RSI CALLS代表了CPU的代价估算包括了键比较compare key以及行估算row evaluating的代价。
为了让你更好地理解我说下关于W和RSI CALLS的英文解释W is an adjustable weight between I/O and CPU utilization. The number of RSI calls is used to approximate CPU utilization。
这样你应该能明白为了让CPU代价和I/O代价放到一起来统计我们使用了转化的系数W
另外需要说明的是在MySQL5.7版本之后代价模型又进行了完善不仅考虑到了I/O和CPU开销还对内存计算和远程操作的代价进行了统计也就是说总代价的计算公式演变成下面这样
总代价 = I/O代价 + CPU代价 + 内存代价 + 远程代价
这里对内存代价和远程代价不进行讲解我们只需要关注I/O代价和CPU代价即可。
## 总结
我今天讲解了查询优化器它在RDBMS中是个非常重要的角色。在优化器中会经历逻辑查询优化和物理查询优化阶段。
最后我们只是简单梳理了下CBO的总代价是如何计算的以及包括了哪些部分。CBO的代价计算是个复杂的过程细节很多不同优化器的实现方式也不同。另外随着优化器的逐渐成熟考虑的因素也会越来越多。在某些情况下MySQL还会把RBO和CBO组合起来一起使用。RBO是个简单固化的模型在Oracle 8i之前采用的就是RBO在优化器中一共包括了15种规则输入的SQL会根据符合规则的情况得出相应的执行计划在Oracle 10g版本之后就用CBO替代了RBO。
CBO中需要传入的参数除了SQL查询以外还包括了优化器参数、数据表统计信息和系统配置等这实际上也导致CBO出现了一些缺陷比如统计信息不准确参数配置过高或过低都会导致路径选择的偏差。除此以外查询优化器还需要在优化时间和执行计划质量之间进行平衡比如一个执行计划的执行时间是10秒钟就没有必要花1分钟优化执行计划除非该SQL使用频繁高后续可以重复使用该执行计划。同样CBO也会做一些搜索空间的剪枝以便在有效的时间内找到一个“最优”的执行计划。这里其实也是在告诉我们为了得到一个事物付出的成本过大即使最终得到了有时候也是得不偿失的。
<img src="https://static001.geekbang.org/resource/image/1b/36/1be7b4fcd9ebdbfcc4f203a6c5e4a836.jpg" alt=""><br>
最后留两道思考题吧RBO和CBO各自的特点是怎样的呢为什么CBO也存在不足你能用自己的话描述一下其中的原因吗
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起来学习进步。