MySQL索引选择不正确并详细解析OPTIMIZER_TRACE格式

一 表结构如下: 

CREATE TABLE t_audit_operate_log (
  Fid bigint(16) AUTO_INCREMENT,
  Fcreate_time int(10) unsigned NOT NULL DEFAULT '0',
  Fuser varchar(50) DEFAULT '',
  Fip bigint(16) DEFAULT NULL,
  Foperate_object_id bigint(20) DEFAULT '0',
  PRIMARY KEY (Fid),
  KEY indx_ctime (Fcreate_time),
  KEY indx_user (Fuser),
  KEY indx_objid (Foperate_object_id),
  KEY indx_ip (Fip)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

执行查询:

mysql> explain select count(*) from t_audit_operate_log where ;
+----------+
| count(*) |
+----------+
| 1238382 | 
+----------+

select count(*) from t_audit_operate_log where Fcreate_time>=1407254400 and Fcreate_time<=1407427199;
+----------+
| count(*) |
+----------+
| 198920 | 
+----------+

显然,使用索引indx_ctime好于indx_user,但MySQL却选择了indx_user. 为什么?

于是,使用 OPTIMIZER_TRACE进一步探索.

 

二  OPTIMIZER_TRACE的过程说明

以本处事例简要说明OPTIMIZER_TRACE的过程.

查看OPTIMIZER_TRACE方法:

1.set optimizer_trace='enabled=on';    --- 开启trace

2.set optimizer_trace_max_mem_size=1000000;    --- 设置trace大小

3.set end_markers_in_json=on;    --- 增加trace中注释

4.select * from information_schema.optimizer_trace\G;

 

{\
  "steps": [\
    {\
      "join_preparation": {\  ---优化准备工作
        "select#": 1,\
        "steps": [\ {\ "expanded_query": "/* select#1 */ select count(0) AS `count(*)` from `t_audit_operate_log` where ((`t_audit_operate_log`.`Fuser` = 'XX@XX.com') and (`t_audit_operate_log`.`Fcreate_time` >= 1407081600) and (`t_audit_operate_log`.`Fcreate_time` <= 1407427199))"\ }\ ] /* steps */\ } /* join_preparation */\ },\ {\ "join_optimization": {\ ---优化工作的主要阶段,包括逻辑优化和物理优化两个阶段 "select#": 1,\ "steps": [\ ---优化工作的主要阶段, 逻辑优化阶段 {\ "condition_processing": {\ ---逻辑优化,条件化简 "condition": "WHERE",\ "original_condition": "((`t_audit_operate_log`.`Fuser` = 'XX@XX.com') and (`t_audit_operate_log`.`Fcreate_time` >= 1407081600) and (`t_audit_operate_log`.`Fcreate_time` <= 1407427199))",\ "steps": [\ {\ "transformation": "equality_propagation",\ ---逻辑优化,条件化简,等式处理 "resulting_condition": "((`t_audit_operate_log`.`Fuser` = 'XX@XX.com') and (`t_audit_operate_log`.`Fcreate_time` >= 1407081600) and (`t_audit_operate_log`.`Fcreate_time` <= 1407427199))"\ },\ {\ "transformation": "constant_propagation",\ ---逻辑优化,条件化简,常量处理 "resulting_condition": "((`t_audit_operate_log`.`Fuser` = 'XX@XX.com') and (`t_audit_operate_log`.`Fcreate_time` >= 1407081600) and (`t_audit_operate_log`.`Fcreate_time` <= 1407427199))"\ },\ {\ "transformation": "trivial_condition_removal",\ ---逻辑优化,条件化简,条件去除 "resulting_condition": "((`t_audit_operate_log`.`Fuser` = 'XX@XX.com') and (`t_audit_operate_log`.`Fcreate_time` >= 1407081600) and (`t_audit_operate_log`.`Fcreate_time` <= 1407427199))"\ }\ ] /* steps */\ } /* condition_processing */\ },\ ---逻辑优化,条件化简,结束 {\ "table_dependencies": [\ ---逻辑优化, 找出表之间的相互依赖关系. 非直接可用的优化方式. {\ "table": "`t_audit_operate_log`",\ "row_may_be_null": false,\ "map_bit": 0,\ "depends_on_map_bits": [\ ] /* depends_on_map_bits */\ }\ ] /* table_dependencies */\ },\ {\ "ref_optimizer_key_uses": [\ ---逻辑优化, 找出备选的索引 {\ "table": "`t_audit_operate_log`",\ "field": "Fuser",\ "equals": "'XX@XX.com'",\ "null_rejecting": false\ }\ ] /* ref_optimizer_key_uses */\ },\ {\ "rows_estimation": [\ ---逻辑优化, 估算每个表的元组个数. 单表上进行全表扫描和索引扫描的代价估算. 每个索引都估算索引扫描代价 {\ "table": "`t_audit_operate_log`",\ "range_analysis": {\ "table_scan": {\---逻辑优化, 估算每个表的元组个数. 单表上进行全表扫描的代价 "rows": 8150516,\ "cost": 1.73e6\ } /* table_scan */,\ "potential_range_indices": [\ ---逻辑优化, 列出备选的索引. 后续版本字符串变为potential_range_indexes {\ "index": "PRIMARY",\---逻辑优化, 本行表明主键索引不可用 "usable": false,\ "cause": "not_applicable"\ },\ {\ "index": "indx_ctime",\---逻辑优化, 索引indx_ctime "usable": true,\ "key_parts": [\ "Fcreate_time",\ "Fid"\ ] /* key_parts */\ },\ {\ "index": "indx_user",\---逻辑优化, 索引indx_user "usable": true,\ "key_parts": [\ "Fuser",\ "Fid"\ ] /* key_parts */\ },\ {\ "index": "indx_objid",\---逻辑优化, 索引 "usable": false,\ "cause": "not_applicable"\ },\ {\ "index": "indx_ip",\---逻辑优化, 索引 "usable": false,\ "cause": "not_applicable"\ }\ ] /* potential_range_indices */,\ "setup_range_conditions": [\ ---逻辑优化, 如果有可下推的条件,则带条件考虑范围查询 ] /* setup_range_conditions */,\ "group_index_range": {\---逻辑优化, 如带有GROUPBY或DISTINCT,则考虑是否有索引可优化这种操作. 并考虑带有MIN/MAX的情况 "chosen": false,\ "cause": "not_group_by_or_distinct"\ } /* group_index_range */,\ "analyzing_range_alternatives": {\---逻辑优化,开始计算每个索引做范围扫描的花费(等值比较是范围扫描的特例) "range_scan_alternatives": [\ {\ "index": "indx_ctime",\ ---[A] "ranges": [\ "1407081600 <= Fcreate_time <= 1407427199"\ ] /* ranges */,\ "index_dives_for_eq_ranges": true,\ "rowid_ordered": false,\ "using_mrr": true,\ "index_only": false,\ "rows": 688362,\ "cost": 564553,\ ---逻辑优化,这个索引的代价最小 "chosen": true\ ---逻辑优化,这个索引的代价最小,被选中. (比前面的table_scan 和其他索引的代价都小) },\ {\ "index": "indx_user",\ "ranges": [\ "XX@XX.com <= Fuser <= XX@XX.com"\ ] /* ranges */,\ "index_dives_for_eq_ranges": true,\ "rowid_ordered": true,\ "using_mrr": true,\ "index_only": false,\ "rows": 1945894,\ "cost": 1.18e6,\ "chosen": false,\ "cause": "cost"\ }\ ] /* range_scan_alternatives */,\ "analyzing_roworder_intersect": {\ "usable": false,\ "cause": "too_few_roworder_scans"\ } /* analyzing_roworder_intersect */\ } /* analyzing_range_alternatives */,\---逻辑优化,开始计算每个索引做范围扫描的花费. 这项工作结算 "chosen_range_access_summary": {\---逻辑优化,开始计算每个索引做范围扫描的花费. 总结本阶段最优的. "range_access_plan": {\ "type": "range_scan",\ "index": "indx_ctime",\ "rows": 688362,\ "ranges": [\ "1407081600 <= Fcreate_time <= 1407427199"\ ] /* ranges */\ } /* range_access_plan */,\ "rows_for_plan": 688362,\ "cost_for_plan": 564553,\ "chosen": true\ -- 这里看到的cost和rows都比 indx_user 要来的小很多---这个和[A]处是一样的,是信息汇总. } /* chosen_range_access_summary */\ } /* range_analysis */\ }\ ] /* rows_estimation */\ ---逻辑优化, 估算每个表的元组个数. 行估算结束 },\ {\ "considered_execution_plans": [\ ---物理优化, 开始多表连接的物理优化计算 {\ "plan_prefix": [\ ] /* plan_prefix */,\ "table": "`t_audit_operate_log`",\ "best_access_path": {\ "considered_access_paths": [\ {\ "access_type": "ref",\ ---物理优化, 计算indx_user索引上使用ref方查找的花费, "index": "indx_user",\ "rows": 1.95e6,\ "cost": 683515,\ "chosen": true\ },\ ---物理优化, 本应该比较所有的可用索引,即打印出多个格式相同的但索引名不同的内容,这里却没有。推测是bug--没有遍历每一个索引. {\ "access_type": "range",\---物理优化,猜测对应的是indx_time(没有实例可进行调试,对比5.7的跟踪信息猜测而得) "rows": 516272,\ "cost": 702225,\---物理优化,代价大于了ref方式的683515,所以没有被选择 "chosen": false\ -- cost比上面看到的增加了很多,但rows没什么变化 ---物理优化,此索引没有被选择 }\ ] /* considered_access_paths */\ } /* best_access_path */,\ "cost_for_plan": 683515,\ ---物理优化,汇总在best_access_path 阶段得到的结果 "rows_for_plan": 1.95e6,\ "chosen": true\ -- cost比上面看到的竟然小了很多?虽然rows没啥变化 ---物理优化,汇总在best_access_path 阶段得到的结果 }\ ] /* considered_execution_plans */\ },\ {\ "attaching_conditions_to_tables": {\---逻辑优化,尽量把条件绑定到对应的表上 } /* attaching_conditions_to_tables */\ },\ {\ "refine_plan": [\ {\ "table": "`t_audit_operate_log`",\---逻辑优化,下推索引条件"pushed_index_condition";其他条件附加到表上做为过滤条件"table_condition_attached" }\ ] /* refine_plan */\ }\ ] /* steps */\ } /* join_optimization */\ \---逻辑优化和物理优化结束 },\ {\ "join_explain": {} /* join_explain */\ }\ ] /* steps */\




可以看到,即便是一条非常简单的SQL,也会打印出很冗长的查询计划。
 
当然你也可以把查询计划导入到文件中去,例如导入到一个命名为xx.trace的文件,然后用JSON阅读器来查看     
 
SELECT TRACE INTO DUMPFILE “xx.trace” FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
 
 
 
之前也写过博客,提及到optimizer trace的相关选项( http://mysqllover.com/?p=470 ):
 

optimizer_trace有两个字段:

“enabled=on,one_line=off” ,可以通过set 进行字符串更新,前者表示打开optimizer_trace,后者表示打印的查询计划是否以一行显示,还是以json树的形式显示
我们可以在session级别来设这这个参数。
 
默认optimizer_trace_limit值为1,因此只会保存一条记录。这个设置需要重连session才能生效,另外一个变量optimizer_trace_offset通常与之配合使用,默认值为-1
 
例如,offset=-1, limit=1将显示最近一次trace
offset=-2,limit=1将显示最近的前一个trace。

offset=-5,limit=5 将最近的5次trace打印出来

 总的来说:
当offset大于0时,则会显示老的从offset开始的limit个trace,也就是说,新的trace没有记下来。

当offset小于0时,则会显示最新的-offset开始的limit个trace,也就是说,只显示新的trace

 注意重设变量会导致trace被清空

另外由于trace数据是存储在内存中的,因此还需要设置optimizer_trace_max_mem_size来限制内存的使用量,否则意外的设置可能导致内存爆掉。这是session级别,不应该设置的过大
 
optimizer_trace_limit和optimizer_trace_offset也影响占用内存大小,但不应该超过OPTIMIZER_TRACE_MAX_MEM_SIZE
 
 
另外,还有个参数 optimizer_trace_features,可以控制打印到查询计划树的项,默认情况下是全部打开的,如下:
 

mysql> show variables like ‘optimizer_trace_features';

+————————–+—————————————————————————-+
| Variable_name            | Value                                                                      |
+————————–+—————————————————————————-+
| optimizer_trace_features | greedy_search=on,range_optimizer=on,dynamic_range=on,repeated_subselect=on |
+————————–+—————————————————————————-+
1 row in set (0.00 sec)

 

 

 
如果你不关心某些查询计划选项,可以将其关闭掉,只打印你关注的,这样可以减小查询计划树的输出,让其更可读一点。
 
greedy_search:对于有N个表的join操作,可能产生N的阶乘的查询计划路径;
range_optimizer:range优化
dynamic_range:dynamic range optimizer(也就是”range checked  for each row”,每个外部列会执行一次range optimizer);如果关闭该选项的话,只有第一次调用JOIN_TAB::SQL_SELECT才被跟踪
repeated_subselect:子查询,如果关闭的话,只有第一次调用Item_subselect 才被跟踪
 
—————————————————-
TODO PLAN:增加阈值(读取的行数,或者执行的时间),超过阈值时,自动将trace导入到某个文件中,这样可以便于在线debug
 
参考:
 
 
 
 
 

转载于:https://www.cnblogs.com/DataArt/p/10232831.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值