在一次Mysql分享中提到过,会将相关的一些知识点整理成相应的文章。由于前段时间忙的不可开交,一直没有时间去整理这些相关内容。但是必定说出来的话,就要去落实。本章内容主要以实践为主,最好是跟着动手实践。这样才能逐步掌握其中奥秘。那么我们开始吧!!!
在做这个实践之前,我们要先安装一下mysql数据库,这边是通过源码的形式进行安装。方便后续的调试跟踪。
#cd /Users/edz/Desktop/src-source/mysql-server/
#git clone https://github.com/mysql/mysql-server.git
#cd mysql-server
#git checkout 5.6.48
当前使用版本为5.6.48,所以我们切到5.6.48版本。
#cd /Users/edz/Desktop/src-source/mysql-server/BUILD
#cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/mysql5.6.48 -DMYSQL_DATADIR=/usr/local/mysql5.6.48/data -DSYSCONFDIR=/usr/local/mysql5.6.48/etc -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_MEMORY_STORAGE_ENGINE=1 -DWITH_READLINE=1 -DMYSQL_UNIX_ADDR=/tmp/mysqld.sock -DMYSQL_TCP_PORT=3306 -DENABLED_LOCAL_INFILE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DEXTRA_CHARSETS=all -DDEFAULT_CHARSET=utf8 -DDEFAULT_COLLATION=utf8_general_ci -DWITH_DEBUG=1 -DWITH_UNIT_TESTS=off
#make
#make install
没有指定mysql安装目录时,在mac系统下默认会安装在/usr/local/mysql目录中。
implicit instantiation of undefined template 'std::basic_stringstream 错误解决:
#include //直接引入头即可
[mysqld]
datadir=/usr/local/mysql5.6.48/data
socket=/usr/local/mysql5.6.48/data/mysql.sock
explicit_defaults_for_timestamp=true
lower_case_table_names=1
symbolic-links=0
[mysqld_safe]
log-error=/data/logs/mariadb.log
pid-file=/data/mysql/mariadb.pid
my.conf内容我存在/usr/local/mysql/my.cnf
#cd /usr/local/mysql5.6.48/
#./scripts/mysql_install_db --defaults-file=/usr/local/mysql5.6.48/etc/my.cnf --user=root --basedir=/usr/local/mysql
初始化数据库过程主要是初始化一些基本数据过程。
#/usr/local/mysql5.6.48/bin/mysqld --defaults-file=/usr/local/mysql5.6.48/etc/my.cnf --user=root &
需要启动数据库服务,可以使用命令“ps aux |grep mysqld”,看一下是否启动成功。
#/usr/local/mysql5.6.48/bin/mysql --socket=/usr/local/mysql5.6.48/data/mysql.sock -u root -h 127.0.0.1 --port 3306 -p
输入自己数据库的密码即可,连接到终端。5.6默认情况是没有设置的,mysql5.7会默认设置。
以上的安装只针对于mysql5.6,如果是mysql5.7以上版本,需要支持boost。mysql版本也会细分为boost版本和非boost版本,非boost版本的需要连带下载boost一块编译。但是我们通过通过-DDOWNLOAD_BOOST参数自动下载并编译。
不知道你有没有碰到过这种情况,一条本来可以执行得很快的语句,却由于 MySQL 选错了索引,而导致执行速度变得很慢?我们可以一起来看一个小实验,看一下我们Mysql5.6中的bug。
创建表语句:
-- 创建数据库
create database t1;
use t1;
-- 新建一张demo1表
create table demo1(id int auto_increment primary key, a int, b int, c int, v varchar(1000), key iabc(a,b,c), key ic(c)) engine = innodb;
-- 插入demo1表数据
insert into demo1 select null,null,null,null,null;
insert into demo1 select null,null,null,null,null from demo1;
insert into demo1 select null,null,null,null,null from demo1;
insert into demo1 select null,null,null,null,null from demo1;
insert into demo1 select null,null,null,null,null from demo1;
insert into demo1 select null,null,null,null,null from demo1;
-- 更新demo1表数据
update demo1 set a=id/2, b=id/4, c=6-id/8, v=repeat('a',1000);
-- 查看使用索引情况1
explain select id from demo1 where a<3 and b in (1, 13) and c>=3 order by c desc limit 2;
-- 查看使用索引情况2
explain select id from demo1 force index (iabc) where a<3 and b in (1, 13) and c>=3 order by c desc limit 2;
执行以上语句后,对比一下“查看使用索引情况1” 与 “查看使用索引情况2”执行结果。
同样的语句,使用同样的索引,但是使用了force index之后选择的执行后影响的行数是不一样的。如果数据量大的话,实际的执行性能也会差别很大。使用range scan显然要优于index scan的全扫描。
在分析这个问题前,我们可以通过一个optimizer_trace进行分析,optimizer_trace主要用于语句的优化器跟踪,可以分析一些语句的问题。
我们可以通过一下sql进行optimizer_trace :
-- 设置optimizer_trace最大内存
SET OPTIMIZER_TRACE_MAX_MEM_SIZE=268435456;
-- 设置开启optimizer_trace
SET optimizer_trace="enabled=on";
select id from demo1 where a<3 and b in (1, 13) and c>=3 order by c desc limit 2;
-- 查看optimizer_trace信息
select * from INFORMATION_SCHEMA.OPTIMIZER_TRACE\G;
以下为“查看使用索引情况1”的sql语句optimizer_trace信息:
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `demo1`.`id` AS `id` from `demo1` where ((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3)) order by `demo1`.`c` desc limit 2"
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))"
},
{
"transformation": "constant_propagation",
"resulting_condition": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))"
}
]
}
},
{
"table_dependencies": [
{
"table": "`demo1`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
]
}
]
},
{
"ref_optimizer_key_uses": [
]
},
{
"rows_estimation": [
{
"table": "`demo1`",
"range_analysis": {
"table_scan": {
"rows": 32,
"cost": 12.5
},
"potential_range_indices": [
{
"index": "PRIMARY",
"usable": false,
"cause": "not_applicable"
},
{
"index": "iabc",
"usable": true,
"key_parts": [
"a",
"b",
"c",
"id"
]
},
{
"index": "ic",
"usable": true,
"key_parts": [
"c",
"id"
]
}
],
"best_covering_index_scan": {
"index": "iabc",
"cost": 7.4718,
"chosen": true
},
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "iabc",
"ranges": [
"NULL < a < 3"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": true,
"rows": 3,
"cost": 1.6146,
"chosen": true
},
{
"index": "ic",
"ranges": [
"3 <= c"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 17,
"cost": 21.41,
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
},
"chosen_range_access_summary": {
"range_access_plan": {
"type": "range_scan",
"index": "iabc",
"rows": 3,
"ranges": [
"NULL < a < 3"
]
},
"rows_for_plan": 3,
"cost_for_plan": 1.6146,
"chosen": true
}
}
}
]
},
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`demo1`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "range",
"rows": 3,
"cost": 2.2146,
"chosen": true
}
]
},
"cost_for_plan": 2.2146,
"rows_for_plan": 3,
"chosen": true
}
]
},
{
"attaching_conditions_to_tables": {
"original_condition": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))",
"attached_conditions_computation": [
{
"table": "`demo1`",
"rechecking_index_usage": {
"recheck_reason": "low_limit",
"limit": 2,
"row_estimate": 3,
"range_analysis": {
"table_scan": {
"rows": 32,
"cost": 40.4
},
"potential_range_indices": [
{
"index": "PRIMARY",
"usable": false,
"cause": "not_applicable"
},
{
"index": "iabc",
"usable": false,
"cause": "not_applicable"
},
{
"index": "ic",
"usable": true,
"key_parts": [
"c",
"id"
]
}
],
"best_covering_index_scan": {
"index": "iabc",
"cost": 7.4718,
"chosen": true
},
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "cannot_do_reverse_ordering"
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "ic",
"ranges": [
"3 <= c"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 17,
"cost": 21.41,
"chosen": false,
"cause": "cost"
}
]
}
}
}
}
],
"attached_conditions_summary": [
{
"table": "`demo1`",
"attached": "((`demo1`.`a` < 3) and (`demo1`.`b` in (1,13)) and (`demo1`.`c` >= 3))"
}
]
}
},
{
"clause_processing": {
"clause": "ORDER BY",
"original_clause": "`demo1`.`c` desc",
"items": [
{
"item": "`demo1`.`c`"
}
],
"resulting_clause_is_simple": true,
"resulting_clause": "`demo1`.`c` desc"
}
},
{
"refine_plan": [
{
"table": "`demo1`",
"access_type": "index_scan"
}
]
},
{
"reconsidering_access_paths_for_index_ordering": {
"clause": "ORDER BY",
"index_order_summary": {
"table": "`demo1`",
"index_provides_order": false,
"order_direction": "undefined",
"index": "unknown",
"plan_changed": false
}
}
}
]
}
},
{
"join_execution": {
"select#": 1,
"steps": [
{
"filesort_information": [
{
"direction": "desc",
"table": "`demo1`",
"field": "c"
}
],
"filesort_priority_queue_optimization": {
"limit": 2,
"rows_estimate": 2656,
"row_size": 22,
"memory_available": 262144,
"chosen": true
},
"filesort_execution": [
],
"filesort_summary": {
"rows": 3,
"examined_rows": 32,
"number_of_tmp_files": 0,
"sort_buffer_size": 90,
"sort_mode": ""
}
}
]
}
}
]
}
join_preparation段落展示了准备阶段的执行过程。
join_optimization展示优化阶段的执行过程,是分析OPTIMIZER TRACE的重点。这段内容超级长,而且分了好多步骤,不妨按照步骤逐段分析:
该段用来做条件处理,主要对WHERE条件进行优化处理。
其中:
substitute_generated_columns用于替换虚拟生成列
table_dependencies段分析表之间的依赖关系
其中:
列出所有可用的ref类型的索引。如果使用了组合索引的多个部分(例如本例,用到了index(from_date, to_date) 的多列索引),则会在ref_optimizer_key_uses下列出多个元素,每个元素中会列出ref使用的索引及对应值。
顾名思义,用于估算需要扫描的记录数。
其中:
负责对比各可行计划的开销,并选择相对最优的执行计划。
其中:
基于considered_execution_plans中选择的执行计划,改造原有where条件,并针对表增加适当的附加条件,以便于单表数据的筛选。
其中:
最终的、经过优化后的表条件。
改善执行计划。
join_execution段落展示了执行阶段的执行过程。
源码主要针对成本计算部分,我们可以来看一下具体有几个计算成本。
除了以上的还有一些其他的,我们本文只会讲这几个。其余的可以参照源码方式进行获取。
根据2.2小节中OPTIMIZER_TRACE的输出,我们可以看rows_estimation,是用来计算一个表在不同的访问路径下(全表扫描、索引扫描、范围扫描等),数据库所要付出的代价。
可以通过lldb下一个test_quick_select断点:
(lldb)b test_quick_select
然后执行打开一个新终端执行“查看使用索引情况1”的sql,断点进入跟踪到下图:
可以看到我们跟中到ha_innobase::scan_time函数,发现我们是在innodb引擎中处理的。发现其实不同到引擎计算规则会有差异。
可以看到图中的“12.5”和2.2小节中OPTIMIZER_TRACE的table_scan一致。
我们可以看一下test_quick_select中的计算代码,在sql/opt_range.cc中:
int SQL_SELECT::test_quick_select(THD *thd, key_map keys_to_use,
table_map prev_tables,
ha_rows limit, bool force_quick_range,
const ORDER::enum_order interesting_order)
{
double scan_time;
//...省略
records= head->file->stats.records; //行数
if (!records)
records++; /* purecov: inspected */
scan_time= records * ROW_EVALUATE_COST + 1;
read_time= head->file->scan_time() + scan_time + 1.1;
//...省略
//trace信息
Opt_trace_context * const trace= &thd->opt_trace;
Opt_trace_object trace_range(trace, "range_analysis");
Opt_trace_object(trace, "table_scan"). //trace 全表扫描
add("rows", head->file->stats.records). //trace 行数
add("cost", read_time); //trace 成本信息
//...省略
}
ROW_EVALUATE_COST宏,在sql/sql_const.h中:
//行代价
#define ROW_EVALUATE_COST 0.20
通过以上信息我们可以得知到mysql5.6.48中的计算成本:
read_time= InnoDB页数 + scan_time + 1.1;
scan_time= 行数 * 行代价 + 1;
那么“行数”怎么获取,其实可以通过:
-- 获取demo1表的表状态
show table status like 'demo1' \G;
那么“InnoDB页数”又怎么获取:
InnoDB页数 = 数据长度 / InnoDB页大小
其实数据长度我们有了,是“65536”。
InnoDB页大小,可以通过show variables获取,通常是16k:
show variables like 'innodb_page_size';
InnoDB页数 = 65536 / 16384 = 4
scan_time= 32 * 0.2 + 1 = 7.4
read_time= 4 + scan_time + 1.1 = 12.5
最终read_time等于12.5,也就是table_scan的cost成本。
注意: 其实在高版本中,比如mysql8.0中scan_time对应cpu_cost,read_time对应io_cost,不过计算公式有所改变。
int SQL_SELECT::test_quick_select(THD *thd, key_map keys_to_use,
table_map prev_tables,
ha_rows limit, bool force_quick_range,
const ORDER::enum_order interesting_order)
{
//...省略
int key_for_use= find_shortest_key(head, &head->covering_keys);
double key_read_time=
param.table->file->index_only_read_time(key_for_use,
rows2double(records)) +
records * ROW_EVALUATE_COST; //计算部分
//...省略
}
根据计算部分我们可以看到index_only_read_time函数,也是计算部分之一,函数原型如下(在sql/handler.cc中):
double handler::index_only_read_time(uint keynr, double records)
{
double read_time;
uint keys_per_block= (stats.block_size/2/
(table_share->key_info[keynr].key_length + ref_length) +
1);
read_time=((double) (records + keys_per_block-1) /
(double) keys_per_block);
return read_time;
}
源码中records * ROW_EVALUATE_COST = 32 * 0.2 = 6.4,这个是行成本。
行成本需要加上read_time,read_time计算就相对比较复杂。
keys_per_block = (索引块大小/2/(键长度+引用长度)+1)
根据图中可以得出:
keys_per_block = (16384/2/(15+4)+1) = 432
432其实是取整后的值。
然后read_time = ((double) (32 + 432 - 1) /
(double) 432);
read_time = 463 / 432 = 1.0717
保留4位小数后等到结果1.0717,然后在加上行成本的6.4,最终得到我们覆盖索引成本的“7.4717”
要得到执行计划代价我们可以下一个断点:
(lldb)b best_access_path
根据上图可以来看一下我们具体的代码实现部分,如下(在sql/sql_planner.cc中):
void Optimize_table_order::best_access_path(
JOIN_TAB *s,
table_map remaining_tables,
uint idx,
bool disable_jbuf,
double record_count,
POSITION *pos,
POSITION *loose_scan_pos)
{
//...省略
const double scan_cost=
tmp + (record_count * ROW_EVALUATE_COST * rnd_records);
trace_access_scan.add("rows", rows2double(rnd_records)).
add("cost", scan_cost);
//...省略
}
scan_cost=
tmp + (record_count * ROW_EVALUATE_COST * rnd_records);
record_count记录计数,当前为1,rnd_records为找到记录数当前为3。tmp计算部分比较复杂,可以留个小作业。看一下best_access_path函数中实现。
最终scan_cost=1.6146+(1 * 0.2 * 3)= 2.2146
2.2146为取小数后4位。
其实解决索引方案可以概括为几种:
Tracing the Optimizer
https://dev.mysql.com/doc/internals/en/optimizer-tracing.html
手把手教你认识OPTIMIZER_TRACE
http://blog.itpub.net/28218939/viewspace-2658978/
MYSQL sql执行过程的一些跟踪分析(二.mysql优化器追踪分析)
http://blog.itpub.net/29863023/viewspace-2565095/
使用 Trace 进行执行计划分析
https://www.cnblogs.com/hbbbs/articles/12737077.html