深入解析:从源码窥探MySQL优化器
关注我们,下载更多资源
作者 | 汤爱中,云和恩墨SQM开发者,Oracle/MySQL/DB2的SQL解析引擎、SQL审核与智能优化引擎的重要贡献者,产品广泛应用于金融、电信等行业客户中。
摘要
优化器是逻辑SQL到物理存储的解释器,是一个复杂而“愚蠢”的数学模型,它的入参通常是SQL、统计信息以及优化器参数等,而输出通常一个可执行的查询计划,因此优化器的优劣取决于数学模型的稳定性和健壮性,理解这个数学模型就能理解数据库的SQL处理流程。
01 优化器的执行流程
(注:此图出自李海翔)
上图展示了优化器的大致执行过程,可以简单描述为:
1 根据语法树及统计统计,构建初始表访问数组(init_plan_arrays)
2 根据表访问数组,计算每个表的最佳访问路径(find_best_ref),同时保存当前最优执行计划(COST最小)
3 如果找到更优的执行计划则更新最优执行计划,否则优化结束。
从上述流程可以看出,执行计划的生成是一个“动态规划/贪心算法”的过程,动态规划公式可以表示为:Min(Cost(Tn+1)) = Min(Cost(T1))+Min(Cost(T2))+...Min(Cost(Tn-1))+Min(Cost(Tn)),其中Cost(Tn)表示访问表T1 T2一直到Tn的代价。如果优化器没有任何先验知识,则需要进行 A(n,n) 次循环,是一个全排列过程,很显然优化器是有先验知识的,如表大小,外连接,子查询等都会使得表的访问是部分有序的,可以理解为一个“被裁减”的动态规划,动态规则的核心函数为:Join::Best_extention_limited_search,在源码中有如下代码结构:
bool Optimize_table_order::best_extension_by_limited_search(
table_map remaining_tables,
uint idx,
uint current_search_depth)
{
for (JOIN_TAB **pos= join->best_ref + idx; *pos; pos++)
{
......
best_access_path(s, remaining_tables, idx, false,
idx ? (position-1)->prefix_rowcount : 1.0,
position);
......
if (best_extension_by_limited_search(remaining_tables_after,
idx + 1,
current_search_depth - 1))
......
backout_nj_state(remaining_tables, s);
......
}
}
以上代码是在一个for循环中递归搜索,这是一个典型的全排列的算法。
02优化器参数
MySQL的优化器对于Oracle来说还显得比较幼稚。Oracle有着各种丰富的统计信息,比如系统统计信息,表统计信息,索引统计信息等,而MySQL则需要更多的常量,其中MySQL5.7提供了表mysql.server_cost和表mysql.engine_cost,可以供用户配置,使得用户能够调整优化器模型,下面就几个常见而又非常重要的参数进行介绍:
1 #define ROW_EVALUATE_COST 0.2f
计算符合条件的行的代价,行数越多,代价越大
2 #define IO_BLOCK_READ_COST 1.0f
从磁盘读取一个Page的代价
3 #define MEMORY_BLOCK_READ_COST 1.0f
从内存读取一个Page的代价,对于Innodb来说,表示从一个Buffer Pool读取一个Page的代价,因此读取内存页和磁盘页的默认代价是一样的
4 #define COND_FILTER_EQUALITY 0.1f
等值过滤条件默认值为0.1,例如name = ‘lily’, 表大小为100,则返回10行数据
5 #define COND_FILTER_INEQUALITY 0.3333f
非等值过滤条件的默认值是0.3333,例如col1>col2
6 #define COND_FILTER_BETWEEN 0.1111f
Between过滤的默认值是0.1111f,例如:col1 between a and b
......
这样的常量很多,涉及到过滤条件、JOIN缓存、临时表等等各种代价,理解这些常量后,看到执行计划的Cost后,你会有种豁然开朗的感觉!
03 优化器选项
在MySQL中,执行select @@optimizer_trace, 得到如下参数:
index_merge=on,index_merge_union=off,index_merge_sort_union=off, index_merge_intersection=on, engine_condition_pushdown=on, index_condition_pushdown=on, mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,subquery_materialization_cost_based=on, use_index_extensions=on, condition_fanout_filter=on
04 Optimize Trace是如何生成的?
在流程图中的函数中,存在大量如下代码:
Opt_trace_object trace_ls(trace, "searching_loose_scan_index");
因此,在优化器运行过程中,优化器的执行路径也被保存在Opt_trace_object中,进而保存在information_schema.optimizer_trace中,方便用户查询和跟踪。
05 优化器的典型使用场景
5.1 全表扫描
select * from sakila.actor;
表actor统计信息如下:
Db_name |
Table_name |
Last_update |
n_rows |
Cluster_index_size |
Other_index |
sakila |
actor |
2018-11-20 16:20:12 |
200 |
1 |
0 |
主键actor_id统计信息如下:
Index_name |
Last_update |
Stat_name |
Stat_value |
Sample_size |
Stat_description |
PRIMARY |
2018-11-14 14:25:49 |
n_diff_pfx01 |
200 |
1 |
actor_id |
PRIMARY |
2018-11-14 14:25:49 |
n_leaf_pages |
1 |
NULL |
Number of leaf pages in the index |
PRIMARY |
2018-11-14 14:25:49 |
size |
1 |
NULL |
Number of pages in the index |
执行计划:
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "41.00"
},
"table": {
"table_name": "actor",
"access_type": "ALL",
"rows_examined_per_scan": 200,
"rows_produced_per_join": 200,
"filtered": "100.00",
"cost_info": {
"read_cost": "1.00",
"eval_cost": "40.00",
"prefix_cost": "41.00",
"data_read_per_join": "56K"
},
"used_columns": [
"actor_id",
"first_name",
"last_name",
"last_update",
"id"
]
}
}
}
IO_COST = CLUSTER_INDEX_SIZE * PAGE_READ_TIME = 1 * 1 =1;
EVAL_COST = TABLE_ROWS*EVALUATE_COST = 200 * 0.2 =40;
PREFIX_COST = IO_COST + EVAL_COST;
注意以上过程忽略了内存页和磁盘页的访问代价差异。
5.2 表连接时使用全表扫描
SELECT
*
FROM
sakila.actor a,
sakila.film_actor b
WHERE a.actor_id = b.actor_id
Db_name |
Table_name |
Last_update |
n_rows |
Cluster_index_size |
Other_index_size |
Sakila |
Film_actor |
2018-11-20 16:55:31 |
5462 |
12 |
5 |
表film_actor中索引(actor_id,film_id)统计信息如下:
Index_name |
Last_update |
Stat_name |
Stat_value |
Sample_size |
Stat_description |
PRIMARY |
2018-11-14 14:25:49 |
n_diff_pfx01 |
200 |
1 |
actor_id |
PRIMARY |
2018-11-14 14:25:49 |
n_diff_pfx02 |
5462 |
1 |
actor_id,film_id |
PRIMARY |
2018-11-14 14:25:49 |
n_leaf_pages |
11 |
NULL |
Number of leaf pages in the index |
PRIMARY |
2018-11-14 14:25:49 |
size |
12 |
NULL |
Number of pages in the index |
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "1338.07"
},
"nested_loop": [
{
"table": {
"table_name": "a",
"access_type": "ALL",
"possible_keys": [
"PRIMARY"
],
"rows_examined_per_scan": 200,
"rows_produced_per_join": 200,
"filtered": "100.00",
"cost_info": {
"read_cost": "1.00",
"eval_cost": "40.00",
"prefix_cost": "41.00",
"data_read_per_join": "54K"
},
"used_columns": [
"actor_id",
"first_name",
"last_name",
"last_update"
]
}
},
{
"table": {
"table_name": "b",
"access_type": "ref",
"possible_keys": [
"PRIMARY"
],
"key": "PRIMARY",
"used_key_parts": [
"actor_id"
],
"key_length": "2",
"ref": [
"sakila.a.actor_id"
],
"rows_examined_per_scan": 27,
"rows_produced_per_join": 5461,
"filtered": "100.00",
"cost_info": {
"read_cost": "204.67",
"eval_cost": "1092.40",
"prefix_cost": "1338.07",
"data_read_per_join": "85K"
},
"used_columns": [
"actor_id",
"film_id",
"last_update"
]
}
}
]
}
}
第一张表actor的全表扫代价为41,可以参考示例1。
第二个表就是NET LOOP 代价:
read_cost(204.67) =prefix_rowcount * (1 + keys_per_value/table_rows*cluster_index_size =
200 * (1+27/13863*12)*1
注意:27 相当于对于每个actor_id,film_actor的索引估计,对于每个actor_id,平均有27条记录=5462/200
Table_rows是如何计算的呢?
Film_actor表的实际记录数是5462,一共12个page,11个叶子页,总大小为11*16K(默认页大小)=180224Byte, 最小记录长度为26(通过计算字段长度可得),13863 = 180224/26*2, 2是安全因子,做最差的代价估计。
表连接返回行数=200*5462/200,因此行估算代价为5462*0.2=1902.4
5.3 IN查询
表film_actor中索引idx_id(film_id)统计信息如下:
Index_name |
Last_update |
Stat_name |
Stat_value |
Sample_size |
Stat_description |
idx_id |
2018-11-14 14:25:49 |
n_diff_pfx01 |
997 |
4 |
actor_id |
idx_id |
2018-11-14 14:25:49 |
n_diff_pfx02 |
5462 |
4 |
film_id,actor_id |
idx_id |
2018-11-14 14:25:49 |
n_leaf_pages |
4 |
NULL |
Number of leaf pages in the index |
idx_id |
2018-11-14 14:25:49 |
size |
5 |
NULL |
Number of pages in the index |
EXPLAIN SELECT * FROM ACTOR WHERE actor_id IN (SELECT film_id FROM film_actor)
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "460.79"
},
"nested_loop": [
{
"table": {
"table_name": "ACTOR",
"access_type": "ALL",
"possible_keys": [
"PRIMARY"
],
"rows_examined_per_scan": 200,
"rows_produced_per_join": 200,
"filtered": "100.00",
"cost_info": {
"read_cost": "1.00",
"eval_cost": "40.00",
"prefix_cost": "41.00",
"data_read_per_join": "56K"
},
"used_columns": [
"actor_id",
"first_name",
"last_name",
"last_update",
"id"
]
}
},
{
"table": {
"table_name": "film_actor",
"access_type": "ref",
"possible_keys": [
"idx_id"
],
"key": "idx_id",
"used_key_parts": [
"film_id"
],
"key_length": "2",
"ref": [
"sakila.ACTOR.actor_id"
],
"rows_examined_per_scan": 5,
"rows_produced_per_join": 200,
"filtered": "100.00",
"using_index": true,
"first_match": "ACTOR",
"cost_info": {
"read_cost": "200.66",
"eval_cost": "40.00",
"prefix_cost": "460.79",
"data_read_per_join": "3K"
},
"used_columns": [
"film_id"
],
"attached_condition": "(`sakila`.`actor`.`actor_id` = `sakila`.`film_actor`.`film_id`)"
}
}
]
}
}
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
------ ----------- ---------- ---------- ------ ------------- ------ ------- --------------------- ------ -------- ---------------------------------------------
1 SIMPLE ACTOR (NULL) ALL PRIMARY (NULL) (NULL) (NULL) 200 100.00 (NULL)
1 SIMPLE film_actor (NULL) ref idx_id idx_id 2
sakila.ACTOR.actor_id 5 100.00 Using where; Using index; FirstMatch(ACTOR)
从执行计划中可以看出,MySQL采用FirstMatch方式。在MySQL中,半链接优化方式为:Materialization Strategy,LooseScan,FirstMatch,DuplicateWeedout,默认情况下四种优化方式都是存在的,选取方式基于最小COST。现在我们以FirstMatch为例,讲解优化器的执行流程。
SQL如下:
select * from Country
where Country.code IN (select City.Country
from City
where City.Population > 1*1000*1000)
and Country.continent='Europe'
从上图可以看出,FirstMatch是通过判断记录是否已经在结果集中存在来减少查询和匹配流程。
表actor的访问代价可以参考示例1.
表film_actor表的访问代价200.66是如何计算的呢?
访问表film_actor中索引字段film_id,MySQL会走覆盖索引扫,即IDEX_ONLY_SCAN,一次索引访问的代价是如何计算的呢?
参考函数double handler::index_only_read_time(uint keynr, double records)
索引块大小为16K,并且MySQL假设块都是半满的,则一个块能够存放的索引记录数为:
16K/2/(索引长度+主键长度(注:二级索引存储的是主键的引用))=16K/2/(2+4)+1=1366,
其中主键为(actor_id,film_id),两个字段都是smallint,占用4个字节,而索引idx_id(film_id)是2个字节,因此每次访问索引的代价为:(5.47+1366-1)/1366 = 1.0032, 访问film_actor表一共需要200次,总访问代价为:200*1.0032=200.66
总代价460.79 = 表actor的访问代价+表film_actor访问代价+行估算代价=
41+200.66+200*1*5.47*1*02,其中两个1分别表示过滤因子,由于两个表均没有过滤条件因此过滤因子都是1。
资源下载
关注公众号:数据和云(OraNews)回复关键字获取
2018DTCC , 数据库大会PPT
2018DTC,2018 DTC 大会 PPT
DBALIFE ,“DBA 的一天”海报
DBA04 ,DBA 手记4 电子书
122ARCH ,Oracle 12.2体系结构图
2018OOW ,Oracle OpenWorld 资料
PRELECTION ,大讲堂讲师课程资料
近期文章
企业数据架构的云化智能重构和变革(含大会PPT)
Oracle研发总裁Thomas Kurian加盟Google Cloud
变与不变: Undo构造一致性读的例外情况
Oracle 18c新特性:动态 Container Map 增强 Application Container 灵活性
Oracle 18c新特性:Schema-Only 帐号提升应用管理安全性
Oracle 18c新特性:多租户舰队 CDB Fleet (含PPT)
为什么看了那么多灾难,还是过不好备份这一关?
深入解析:从源码窥探MySQL优化器相关推荐
- 【源码】均衡优化器Equilibrium Optimizer(EO)
EO是受控制体积质量平衡的启发来估计动态和平衡状态的. EO is inspired by control volume mass balance to estimate both dynamic a ...
- Centos7源码安装mysql及读写分离,互为主从
Linux服务器 -源码安装mysql 及读写分离,互为主从 一.环境介绍: Linux版本: CentOS 7 64位 mysq版本: mysql-5.6.26 这是我安装时所使用的版本, ...
- PHP自适应小说网站源码深度SEO优化自动采集
深度SEO优化自动采集PHP自适应小说网站源码,此源码是深度SEO优化自动采集的新版本,小说不占内存,存个上万小说不成问题. 记住采集以后的文章需要处理文章信息,至于自动采集我没去细细研究,跟前面的版 ...
- linux怎么用源码安装mysql,Linux源码安装mysql步骤
创建文件夹: mkdir /usr/local/webserver 安装必要依赖包 yum -y install gcc gcc-c++ make ncurses-devel 安装cmake包: t ...
- gcc编译器和mysql源码哪个难_源码编译mysql 5.5
http://blog.csdn.net/aidenliu/article/details/6586610 源码编译mysql 5.5+ 安装过程全记录 分类: Mysql 2011-07-05 21 ...
- l源码安装mysql升级_[Linux]javaEE篇:源码安装mysql
javaEE :源码安装mysql 安装环境 系统平台:CentOS-7-x86_64 数据库版本:mysql-5.6.14 源码安装mysql步骤: 一.卸载mysql 安装mysql之前,先确保l ...
- mysql 安装_源码安装mysql
源码安装mysql 什么是源码 #! /bin/bash echo 'hello'高级语言 ➡️机器码01001001 源码安装mysql逻辑 1.源码包 ⬇ 2.预编译 1.检查当前的操作系统. 2 ...
- dockerfile源码安装mysql_docker容器详解五: dockerfile实现tomcat环境以及源码安装mysql...
tomcat 上一节讲到了dockerfile的基础,这一次咱们来作一个小的练习 首先要了解tomcat安装的整个过程 首先搭建 jdk环境: 下载jdk包,解压以后添加环境变量 而后搭建tomcat ...
- Linux源码安装mysql 5.6.12(cmake编译)
转载链接:http://www.2cto.com/database/201307/229260.html Linux源码安装mysql 5.6.12(cmake编译) 1.安装make编译器(默认系统 ...
最新文章
- linux守护进程中多线程实现,Linux下实现多线程客户/服务器
- 从零开始一个http服务器(五)-模拟cgi
- 性能翻倍 IBM借DS3500拓中低端存储市场
- 理解Promise (3)
- tp5防止sql注入mysql_TP5框架 《防sql注入、防xss攻击》
- 浅谈巴拿马电源的谐波消除原理
- 铭感文件目录_waf绕过
- java完数流程图_编程基本功训练:流程图画法及练习
- 每日一题:leetcode959.由斜杠划分区域
- html单行元素居中显示,多行元素居左显示
- 单片机读tf卡c语言程序,单片机读写U盘闪盘超精简C源程序
- Android开机广播和关机广播
- 境内外赌博网站被捣毁,程序员被抓!!
- 6-4 使用函数统计指定数字的个数_高手不可不学的Excel引用函数(上)
- String源码分析,中高级Java开发面试题
- 从电影《心灵捕手》谈起
- R语言数学表达式、特殊符号等
- 益丰大药房互联网医院,积极推动中国大健康产业发展变革
- Intel TBB的学习动态并行
- Unity实现边缘轮廓高亮
热门文章
- 数字化转型 数字分析_数字化转型的人员问题
- 开源素材网_22个用于广告素材的开源工具
- 汇编edx_开源社区开始热议edX
- 2014年图灵奖_2014年人民选择奖:投下您的一票
- (4)vue.js 基础语法
- CSS 兼容浏览器的方法 CSS Hack
- Integer 数据类型
- yum mysql 如何启动_CentOS7用yum安装MySQL与启动
- mysql触发器_MySQL视图\触发器\事务初步认识
- sybase不支持的条件表达式_包教包会!7段代码带你玩转Python条件语句(附代码)...