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 Fuser='XX@XX.com' and Fcreate_time>=1407081600 and Fcreate_time<=1407427199\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: t_audit_operate_log

type: ref

possible_keys: indx_ctime,indx_user

key: indx_user

key_len: 153

ref: const

rows: 2007326

Extra: Using where

 

發現,使用了一個不合適的索引, 不是很理想,於是改成指定索引:

mysql> explain select count(*) from t_audit_operate_log use index(indx_ctime) where Fuser='CY6016@cyou-inc.com' and Fcreate_time>=1407081600 and Fcreate_time<=1407427199\G

*************************** 1. row ***************************

id: 1

select_type: SIMPLE

table: t_audit_operate_log

type: range

possible_keys: indx_ctime

key: indx_ctime

key_len: 5

ref: NULL

rows: 670092

Extra: Using where

實際執行耗時,后者比前者快了接近10

問題: 很奇怪,優化器為何不選擇使用 indx_ctime 索引,而選擇了明顯會掃描更多行的 indx_user 索引。

分析2個索引的數據量如下:  兩個條件的唯一性對比:

select count(*) from t_audit_operate_log where Fuser='XX@XX.com';
+----------+
| 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;
 
官方文檔給出了一個不錯的例子,比這里的這個要復雜多了,有興趣的同學可自行翻閱: http://dev.mysql.com/doc/internals/en/tracing-example.html
 
 
之前也寫過博客,提及到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
 
參考:
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM