成本模式
传统关系型数据库里面的优化器分为CBO(Cost_Based Optimizer)和RBO(Rule-Based Optimizer)两种方式。
-
RBO所用的判断规则是一组内置的规则,这些规则是硬编码在数据库的编码中的,RBO会根据这些规则去从SQL诸多的路径中来选择一条作为执行计划。RBO最大问题在于硬编码在数据库里面的一系列固定规则,来决定执行计划。并没有考虑目标SQL中所涉及的对象的实际数量,实际数据的分布情况,这样一旦规则不适用于该SQL,那么很可能选出来的执行计划就不是最优执行计划。
-
CBO规则会从目标诸多的执行路径中选择一个成本最小的执行路径来作为执行计划。这里的成本实际代表了根据相关统计信息计算出来目标SQL对应的步骤的IO,CPU等消耗。也就是意味着执行目标SQL所需要IO,CPU等资源的一个估计值。
MySQL优化器使用CBO成本模型,该模型基于对查询执行期间发生的各种操作成本的估计。优化器有一组编译后的默认“成本常数”,可用于制定有关执行计划的决策。
优化器还有一个在执行计划构建期间使用的成本估计数据库。这些估算值存储在mysql系统数据库中的server_cost和engine_cost表中,并且可以在任何时候进行配置。这些表的目的是方便地调整优化器在试图达到查询执行计划时使用的成本估算。
工作原理
-
服务启动时将成本模型表读入内存,并在运行时使用内存中的值。表中指定的任何非null开销估计都优先于相应的编译内的默认开销常数。任何NULL估计都指示优化器使用编译后的默认值。
-
在运行时,服务可能会重新读取成本表。或 DBA能通过更改表中的条目轻松地调整成本估算。当动态加载存储引擎或执行FLUSH OPTIMIZER_COSTS语句时,进行刷新内存数据。
-
客户端会话开始时的当前内存成本估计在整个会话中都适用,直到会话结束。特别是,如果服务重新读取成本表,任何更改的估算只适用于随后启动的会话。现有会话不受影响。
-
成本表特定于给定的实例。不会将成本表更改复制到副本节点。
组成
因MySQL体系结构中分成两层结构,优化器成本模型由来两层组成,这里包含查询执行期间发生的操作的成本估计信息。Server层进行连接管理、权限链接,查询缓存(8.0已废弃)、语法解析、查询优化ICP等操作。在存储引擎层执行具体的数据存取操作。
- server_cost:一般服务层操作的优化器成本估算
- engine_cost:用于存储引擎的操作的优化器成本估算。
mysql> SELECT * FROM mysql.server_cost;
+------------------------------+------------+---------------------+---------+---------------+
| cost_name | cost_value | last_update | comment | default_value |
+------------------------------+------------+---------------------+---------+---------------+
| disk_temptable_create_cost | NULL | 2021-05-01 19:06:35 | NULL | 20 |
| disk_temptable_row_cost | NULL | 2021-05-01 19:06:35 | NULL | 0.5 |
| key_compare_cost | NULL | 2021-05-01 19:06:35 | NULL | 0.05 |
| memory_temptable_create_cost | NULL | 2021-05-01 19:06:35 | NULL | 1 |
| memory_temptable_row_cost | NULL | 2021-05-01 19:06:35 | NULL | 0.1 |
| row_evaluate_cost | NULL | 2021-05-01 19:06:35 | NULL | 0.1 |
+------------------------------+------------+---------------------+---------+---------------+
6 rows in set (0.00 sec)
mysql> SELECT * FROM mysql.engine_cost;
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| engine_name | device_type | cost_name | cost_value | last_update | comment | default_value |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
| default | 0 | io_block_read_cost | NULL | 2021-05-01 19:06:35 | NULL | 1 |
| default | 0 | memory_block_read_cost | NULL | 2021-05-01 19:06:35 | NULL | 0.25 |
+-------------+-------------+------------------------+------------+---------------------+---------+---------------+
2 rows in set (0.00 sec)
server_cost表说明:
列 | 说明 |
---|---|
cost_name | 主键,本模型中使用的成本估计的名称。名称不区分大小写。 |
cost_value | 成本估算值。如果该值非null,则服务器使用它作为开销。否则,它使用默认的估计值 |
last_update | 最后一行更新的时间。 |
comment | 成本估计相关的描述性注释 |
default_value | 成本估算的默认(编译)值。此列是只读生成的列,即使更改了相关的成本估算,它也会保留其值。对于在运行时添加到表中的行,此列的值为NULL。 |
-
disk_temptable_create_cost, disk_temptable_row_cost
内部创建的临时表存储在基于磁盘的存储引擎(InnoDB或MyISAM)中的成本估算。增加这些值会增加使用内部临时表的成本估计,并使优化器更喜欢使用较少的查询计。 -
key_compare_cost
比较记录键的开销。增加这个值会导致比较多个键的查询计划变得更昂贵。例如,与使用索引避免排序的查询计划相比,执行文件排序的查询计划会相对昂贵一些。 -
memory_temptable_create_cost, memory_temptable_row_cost
存储在MEMORY存储引擎中的内部创建的临时表的成本估算。增加这些值会增加使用内部临时表的成本估计,并使优化器更喜欢使用较少的查询计划。
与相应磁盘参数的默认值(disk_temptable_create_cost、disk_temptable_row_cost)相比,内存参数的默认值越小,反映处理基于内存的表的成本越低。 -
row_evaluate_cost
评估记录条件的成本。与检查行数较少的查询计划相比,增加该值将导致检查多行的查询计划的开销增加。例如,与读取更少行的范围扫描相比,表扫描的开销相对更高。
engine_cost表说明:
列 | 说明 |
---|---|
engine_name | 应用此成本估算的存储引擎的名称,名称不区分大小写。 |
device_type | 成本估算适用的设备类型。该列用于指定不同存储设备类型(例如:机械盘或固态盘,PCIE卡之类的)的不同成本估算。但目前没有进行区分。 |
cost_name | 主键,成本估算的存储引擎的名称,名称不区分大小写。如果该值为default,则适用于所有没有命名项的存储引擎。 |
cost_value | 成本估算值。如果该值非null,则服务器使用它作为开销。否则,它使用默认的估计值 |
last_update | 最后一行更新的时间。 |
comment | 成本估计相关的描述性注释。 |
default_value | 成本估算的默认(编译)值。此列是只读生成的列,即使更改了相关的成本估算,它也会保留其值。对于在运行时添加到表中的行,此列的值为NULL。 |
备注:engine_cost表的主键是一个包含(cost_name, engine_name, device_type)列的元组
目前记录里存在两个
-
io_block_read_cost
从磁盘读取索引或数据块的开销。增加该值将导致读取许多磁盘块的查询计划比读取较少磁盘块的查询计划开销更大。例如,与读取更少块的范围扫描相比,表扫描的开销相对更大。 -
emory_block_read_cost
类似于io_block_read_cost,但表示从内存中的数据库缓冲区读取索引或数据块的开销。
如果io_block_read_cost和memory_block_read_cost值不同,执行计划可能会在同一查询的两次运行之间发生变化。假设内存访问的开销小于磁盘访问的开销。在这种情况下,在服务启动时,在数据被读入缓冲池之前,能得到一个与查询运行后不同的计划,因为那时数据在内存中。
查询成本
对于一台服务器设备成本来说成本有 网络,磁盘(I/O),CPU,内存 等组成。但对于MySQL中一条查询SQL语句的执行成本是由以下两个方面组成:
- I/O 成本
MySQL中的数据和索引都存储到磁盘上,当查询表中的记录时,需要先把数据或者索引加载到内存中然后再操作,这个从磁盘到内存这个加载的过程损耗的时间称为I/O成本。 - CPU成本
读取以及检测记录是否满足对应的搜索条件,对结果集进行排序,聚合等这些操作损耗的时间称之为CPU成本。
源码里对于CPU cost定义
CPU cost must be comparable to that of an index scan as computed
in test_quick_select(). When the groups are small,
e.g. for a unique index, using index scan will be cheaper since it
reads the next record without having to re-position to it on every
group. To make the CPU cost reflect this, we estimate the CPU cost
as the sum of:
1. Cost for evaluating the condition (similarly as for index scan).
2. Cost for navigating the index structure (assuming a b-tree).
Note: We only add the cost for one comparison per block. For a
b-tree the number of comparisons will be larger.
TODO: This cost should be provided by the storage engine.
Add CPU cost for processing records (see
@handler::multi_range_read_info_const()).
*/
cost->add_cpu(
table->cost_model()->row_evaluate_cost(static_cast<double>(rows)));
return false;
当这些成本估计出之后,MySQL的优化器会选择其中成本最低,或者说代价最低的那种方案。而成本值是根据索引,表,行的统计信息计算出来的。
全表扫描成本
已employees为基础计算出全表扫描的代价:
mysql> SHOW TABLE STATUS LIKE 'employees'\G;
*************************** 1. row ***************************
Name: employees
Engine: InnoDB
Version: 10
Row_format: Dynamic
Rows: 299512
Avg_row_length: 50
Data_length: 15220736
Max_data_length: 0
Index_length: 15761408
Data_free: 7340032
Auto_increment: NULL
Create_time: 2021-05-01 20:48:45
Update_time: NULL
Check_time: NULL
Collation: utf8mb4_bin
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)
ERROR:
No query specified
mysql> EXPLAIN FORMAT=JSON select * from employees ;
+--------------------------------------------------------------------------------------------+
| EXPLAIN |
+--------------------------------------------------------------------------------------------+
| {
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "30183.45"
},
"table": {
"table_name": "employees",
"access_type": "ALL",
"rows_examined_per_scan": 299512,
"rows_produced_per_join": 299512,
"filtered": "100.00",
"cost_info": {
"read_cost": "232.25", # IO成本
"eval_cost": "29951.20", # CPU成本
"prefix_cost": "30183.45", # 单独查询表的成本
"data_read_per_join": "38M" # 此次查询中需要读取的数据量
},
"used_columns": [
"emp_no",
"birth_date",
"first_name",
"last_name",
"gender",
"hire_date"
]
}
}
} |
+--------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
通过Rows和Data_length两个值
Rows:表示表中的记录条数。对于使用 MyISAM 存储引擎的表来说,该值是准确的,对于使用 InnoDB 存储引擎的表来说,该值是一个估计值。
Data_length:表示表占用的存储空间字节数。使用 MyISAM 存储引擎的表来说,该值就是数据文件的大小,对于使用 InnoDB 存储引擎的表来说,该值就相当于聚簇索引占用的存储空间大小,也就是说可以这样计算该值的大小:
Data_length = 聚簇索引的页面数量 x 每个页面的大小
mysql的默认page大小为16KB。
CPU成本:
eval_cost= Rows * mysql.server_cost(row_evaluate_cost)
29951.20 = 299512 * 0.1
IO成本:
#这里没有采取io_block_read_cost ,因为数据已经在内存中。所以不需要重硬盘读取数据
read_cost = pages * mysql.engine_cost(memory_block_read_cost)
232.25 = 15220736/ 16 /1024 * 0.25
总查询成本:
query_cost = eval_cost + read_cost
30183.45 = 29951.20 + 232.25
对于InnoDB存储引擎来说,全表扫描的意味着把聚簇索引中的记录都依次和给定的搜索条件做一下比较,把符合搜索条件的记录加入到结果集,所以需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。
所以计算全表扫描的代价需要两个信息:聚簇索引占用的页面数,该表中的记录数,页面数用于计算I/O成本,记录数用于计算CPU成本。
查询成本 = I/O 成本+CPU 成本
↓
I/O成本 = 页面数量 x engine_cost表中io_block_read_cost的值
CPU成本 = 记录数 x server_cost表中row_evaluate_cost的值
其他
MySQL的成本模型定义中其实存在4种类型,目前代码实现上只分析到cpu 和 io 模型:
class Cost_estimate {
private:
double io_cost; ///< cost of I/O operations
double cpu_cost; ///< cost of CPU operations
double import_cost; ///< cost of remote operations
double mem_cost; ///< memory used (bytes)
对于cpu_cost只需要获取对应的rows 就可以计算出来,其中复杂的还是属于io_cast
io_cost 定义了以下三种方法: 表扫、索引扫、读取消耗 ,这部分计算方式比较复杂。
##全表扫描
Cost_estimate handler::table_scan_cost() {
const double io_cost = scan_time() * table->cost_model()->page_read_cost(1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
#索引扫描
Cost_estimate handler::index_scan_cost(uint index,
double ranges [[maybe_unused]],
double rows) {
assert(ranges >= 0.0);
assert(rows >= 0.0);
const double io_cost = index_only_read_time(index, rows) *
table->cost_model()->page_read_cost_index(index, 1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
#读取消耗
Cost_estimate handler::read_cost(uint index, double ranges, double rows) {
/*
This function returns a Cost_estimate object. The function should be
implemented in a way that allows the compiler to use "return value
optimization" to avoid creating the temporary object for the return value
and use of the copy constructor.
*/
assert(ranges >= 0.0);
assert(rows >= 0.0);
const double io_cost =
read_time(index, static_cast<uint>(ranges), static_cast<ha_rows>(rows)) *
table->cost_model()->page_read_cost(1.0);
Cost_estimate cost;
cost.add_io(io_cost);
return cost;
}
对于目前提供的cost模型,可通过更改指标,获取新的执行计划。
FLUSH_OPTIMIZER_COSTS:
重新读取成本模型表,以便优化器开始使用存储在其中的当前成本估算。对于任何无法识别的成本模型表项,服务器都会向错误日志中写入一个警告。此操作只影响在刷新之后开始的会话。现有的会议继续使用会议开始时的费用概算。
UPDATE mysql.engine_cost
SET cost_value = 2.0
WHERE cost_name = 'io_block_read_cost';
FLUSH OPTIMIZER_COSTS;
INSERT INTO mysql.engine_cost
VALUES ('InnoDB', 0, 'io_block_read_cost', 3.0,
CURRENT_TIMESTAMP, 'Using a slower disk for InnoDB');
FLUSH OPTIMIZER_COSTS;
总结
目前对于MySQL来说 执行计划还是比较准确的,但也会在碰到执行计划不准确情况。这个取决于优化器的成本模型的设计是否完善、是否科学 直接决定着优化器计算构建出执行计划是否准确,是否达到了最优的选择。随着版本越来越迭代更新,逐步在完善,智能化中,如:MySQL8.0 Autopilot功能(自动预配和自动查询执行计划改善)。