1. 引言
mysql的sql server在根据where condition检索数据的时候,一般会有多种数据检索的方法,其会根据各种数据检索方法代价的大小,选择代价最小的那个数据检索方法。
比如说这个语句,where col1=x and col2=y and col3 >z , 同时存在inx_col1,inx_col2,inx_col3,inx_col1_col2_col3这四个索引,sql server要解决的问题有1)选择哪个索引、 2)是索引range扫描还是ref扫描、3)table scan的方式是否可行。
mysql会根据以下几种数据检索策略选择代价最小的策略来从数据表中获取数据,1)各个索引的range scan代价 2)各个索引的ref scan代价 3)table scan的代价。如何计算这些代价,是本文详细说明的重点。
总代价cost = cpu cost + io cost。
2 . 代价因子
mysql的代价因子在内存中有一份副本,由Server_cost_constants 和SE_cost_constants两个类组成。这两个类的具体数据成员如下。
Mysql Server 代价因子
Server_cost_constants { m_row_evaluate_cost //行记录条件谓词评估代价 m_key_compare_cost //键值比较代价 m_memory_temptable_create_cost //内存临时表创建代价 m_memory_temptable_row_cost //内存临时表的行代价 m_disk_temptable_create_cost //磁盘临时表创建代价 m_disk_temptable_row_cost }
存储引擎代价因子
SE_cost_constants{ m_memory_block_read_cost //从buffer pool中读取一个页面的代价 m_io_block_read_cost //从文件系统中读取一个页面的代价,buffer miss的场景 m_memory_block_read_cost_default m_io_block_read_cost_default }
mysql的代价因子在系统的持久化系统表中也有一份副本,对应mysql.server_cost 和 mysql.engine_cost两个表,这两个表中的字段与 内存中的类字段相同。DBA可以根据实际的硬件情况测试,测试出最适合的代价因子,然后update系统表中对应的字段。再然后执行flush OPTIMIZER_COSTS命令,将修改反应到内存中数据,这样新连接上来的mysql session会读取到内存中数据,然后以新的代价因子计算代价数。
代价因子如何根据实际的硬件环境与负载压力自适应地调整,是一个重要的研究课题。
3 . 统计信息
sql server需要的统计信息是由存储引擎innodb提供的,调用innodb提供的api可以获取这些统计信息,本文的后半部分会罗列这些api。innodb的统计信息根据需要可以持久化到系统表中。mysql.innodb_table_stats和mysql.innodb_index_stats存储了表的统计信息和索引的统计信息。
mysql.innodb_table_stats表中字段说明
database_name 库名 table_name 表名 n_rows 表中的数据行数 clustered_index_size 聚集索引的页面数 sum_of_other_index_sizes 其他非主键索引的页面数 last_update 最后更新这张表的时间
mysql.innodb_index_stats 表中字段说明
database_name 库名
table_name 表名
index_name 索引名
stat_name 统计项名称
stat_value 统计项值
sample_size 采样的页面数
last_update 最后更新这张表的时间
其中stat_name 统计项名称包括:
n_diff_pfxNN 为不同前缀列的cardinality,即不同前缀字段的 distinct value个数
n_leaf_page 索引叶子节点页面数目
size 索引页面数目
4. 代价的计算公式
cpu代价计算
double row_evaluate_cost(double rows) { return rows * m_server_cost_constants->row_evaluate_cost(); }
table scan IO代价计算
Cost_estimate handler::table_scan_cost() { double io_cost= scan_time() * table->cost_model()->page_read_cost(1.0); }
ref and range scan IO代价计算
聚集索引扫描IO代价计算公式
Cost_estimate handler::read_cost(uint index, double ranges, double rows) { double io_cost= read_time(index, static_cast<uint>(ranges), static_cast<ha_rows>(rows)) * table->cost_model()->page_read_cost(1.0); }
二级索引覆盖扫描(不需要回表)IO代价计算公式
Cost_estimate handler::index_scan_cost(uint index, double ranges, double rows) { double io_cost= index_only_read_time(index, rows) * table->cost_model()->page_read_cost_index(index, 1.0); }
二级索引非覆盖扫描(需要回表)IO代价计算公式
min( table→cost_model()→page_read_cost(tmp_fanout), tab→worst_seeks )
估算读取 pages个聚集索引页面所花费的代价, page数乘以代价因子
double Cost_model_table::page_read_cost(double pages)
估算读取 pages个指定 index索引页面所花费的代价数。
double Cost_model_table::page_read_cost_index(uint index, double pages)
5. innodb统计信息api
全表扫描聚集索引时,聚集索引(主键)占用的所有页面数
double ha_innobase::scan_time()
估算在聚集索引上,扫描 rows 条记录,需要读取的页面数
double ha_innobase::read_time(uint index, double ranges, double rows)
估算在指定 keynr索引进行覆盖扫描(不需要回表),扫描 records条记录,需要读取的索引页面数
double handler::index_only_read_time(uint keynr, double records)
估算指定 keynr索引在范围(min_key,max_key)中的记录数量
ha_innobase::records_in_range( uint keynr, /*!< in: index number */ key_range *min_key, /*!< in: start key value of the key_range *max_key) /*!< in: range end key val, may )
估算聚集索引内存中页面数占其所有页面数的比率
double handler::table_in_memory_estimate()
估算二级索引内存中页面数占其所有页面数的比率
double handler::index_in_memory_estimate(uint keyno)
6.开启优化器跟踪
set session optimizer_trace="enabled=on"; explain your sql select * from information_schema.optimizer_trace;
7.优化器跟踪示例
"rows_estimation": [ { "table": "`tab`", "range_analysis": { "table_scan": { "rows": 5, "cost": 4.1 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": false, "cause": "not_applicable" }, { "index": "inx_clo2", "usable": true, "key_parts": [ "clo2", "clo1" ] }, { "index": "inx_clo3", "usable": true, "key_parts": [ "clo3", "clo1" ] }, { "index": "inx_clo2_clo3", "usable": true, "key_parts": [ "clo2", "clo3", "clo1" ] } ], "best_covering_index_scan": { "index": "inx_clo2_clo3", "cost": 2.0606, "chosen": true }, "setup_range_conditions": [ ], "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" }, "analyzing_range_alternatives": { "range_scan_alternatives": [ { "index": "inx_clo2", "ranges": [ "hu <= clo2 <= hu" ], "index_dives_for_eq_ranges": true, "rowid_ordered": true, "using_mrr": false, "index_only": false, "rows": 2, "cost": 3.41, "chosen": false, "cause": "cost" }, { "index": "inx_clo3", "ranges": [ "huan <= clo3 <= huan" ], "index_dives_for_eq_ranges": true, "rowid_ordered": true, "using_mrr": false, "index_only": false, "rows": 1, "cost": 2.21, "chosen": false, "cause": "cost" }, { "index": "inx_clo2_clo3", "ranges": [ "hu <= clo2 <= hu AND huan <= clo3 <= huan" ], "index_dives_for_eq_ranges": true, "rowid_ordered": true, "using_mrr": false, "index_only": true, "rows": 1, "cost": 1.21, "chosen": true } ], "analyzing_roworder_intersect": { "intersecting_indexes": [ { "index": "inx_clo2_clo3", "index_scan_cost": 1, "cumulated_index_scan_cost": 1, "disk_sweep_cost": 0, "cumulated_total_cost": 1, "usable": true, "matching_rows_now": 1, "isect_covering_with_this_index": true, "chosen": true } ], "clustered_pk": { "clustered_pk_added_to_intersect": false, "cause": "no_clustered_pk_index" }, "chosen": false, "cause": "too_few_indexes_to_merge" } }, "chosen_range_access_summary": { "range_access_plan": { "type": "range_scan", "index": "inx_clo2_clo3", "rows": 1, "ranges": [ "hu <= clo2 <= hu AND huan <= clo3 <= huan" ] }, "rows_for_plan": 1, "cost_for_plan": 1.21, "chosen": true } } } ] }, { "considered_execution_plans": [ { "plan_prefix": [ ], "table": "`tab`", "best_access_path": { "considered_access_paths": [ { "access_type": "ref", "index": "inx_clo2", "rows": 2, "cost": 2.4, "chosen": true }, { "access_type": "ref", "index": "inx_clo3", "rows": 1, "cost": 1.2, "chosen": true }, { "access_type": "ref", "index": "inx_clo2_clo3", "rows": 1, "cost": 1.2, "chosen": false }, { "rows_to_scan": 1, "access_type": "range", "range_details": { "used_index": "inx_clo2_clo3" }, "resulting_rows": 1, "cost": 1.41, "chosen": false } ] }, "condition_filtering_pct": 40, "rows_for_plan": 0.4, "cost_for_plan": 1.2, "chosen": true } ] }, { "attaching_conditions_to_tables": { "original_condition": "((`tab`.`clo2` = 'hu') and (`tab`.`clo3` = 'huan'))", "attached_conditions_computation": [ ], "attached_conditions_summary": [ { "table": "`tab`", "attached": "(`tab`.`clo2` = 'hu')" } ] } }, { "refine_plan": [ { "table": "`tab`" } ] } ]