相關(guān)學(xué)習(xí)推薦:mysql教程
前言
- MySQL索引底層數(shù)據(jù)結(jié)構(gòu)與算法
- MySQL性能優(yōu)化原理-前篇
- 實(shí)踐(1)–MySQL性能優(yōu)化
上一篇 《實(shí)踐(1)–MySQL性能優(yōu)化》我們講了數(shù)據(jù)庫(kù)表設(shè)計(jì)的一些原則,Explain工具的介紹、SQL語(yǔ)句優(yōu)化索引的最佳實(shí)踐,本篇繼續(xù)來(lái)聊聊 MySQL 如何選擇合適的索引。
MySQL Trace 工具
MySQL 最終是否選擇走索引或者一張表涉及多個(gè)索引,最終是如何選擇索引,可以使用 trace 工具來(lái)一查究竟,開(kāi)啟 trace工具會(huì)影響 MySQL 性能,所以只能臨時(shí)分析 SQL 使用,用完之后立即關(guān)閉。
案例分析
講 trace 工具之前我們先來(lái)看一個(gè)案例:
# 示例表CREATE TABLE`employees`(`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(24) NOT NULL DEFAULT '' COMMENT '姓名',`age` int(11) NOT NULL DEFAULT '0' COMMENT '年齡',`position` varchar(20) NOT NULL DEFAULT '' COMMENT '職位',`hire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入職時(shí)間', PRIMARY KEY (`id`), KEY `idx_name_age_position` (`name`,`age`,`position`) USING BTREE )ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='員工記錄表'; INSERT INTO employees(name,age,position,hire_time)VALUES('ZhangSan',23,'Manager',NOW());INSERT INTO employees(name,age,position,hire_time)VALUES('HanMeimei', 23,'dev',NOW());INSERT INTO employees(name,age,position,hire_time) VALUES('Lucy',23,'dev',NOW());復(fù)制代碼
MySQL 如何選擇合適的索引
EXPLAIN select * from employees where name > 'a';復(fù)制代碼

如果用name索引需要遍歷name字段聯(lián)合索引樹(shù),然后還需要根據(jù)遍歷出來(lái)的主鍵值去主鍵索引樹(shù)里再去查出最終數(shù)據(jù),成本比全表掃描還高,可以用覆蓋索引優(yōu)化,這樣只需要遍歷name字段的聯(lián)合索引樹(shù)就能拿到所有結(jié)果,如下:
EXPLAIN select name,age,position from employees where name > 'a' ;復(fù)制代碼

EXPLAIN select * from employees where name > 'zzz' ;復(fù)制代碼
對(duì)于上面這兩種 name>'a'
和 name>'zzz'
的執(zhí)行結(jié)果,mysql最終是否選擇走索引或者一張表涉及多個(gè)索引,mysql最終如何選擇索引,我們可以用trace工具來(lái)一查究竟,開(kāi)啟trace工具會(huì)影響mysql性能,所以只能臨時(shí)分析sql使用,用完之后立即關(guān)閉。
trace工具用法
開(kāi)啟/關(guān)閉Trace
#開(kāi)啟traceset session optimizer_trace="enabled=on",end_markers_in_json=on; #關(guān)閉traceset session optimizer_trace="enabled=off";復(fù)制代碼
案例1
執(zhí)行這兩句sql
select * from employees where name >'a' order by position;sELECT * FROM information_schema.OPTIMIZER_TRACE; 復(fù)制代碼
提出來(lái)trace值,詳見(jiàn)注釋
{ "steps": [ { "join_preparation": { --第一階段:SQL準(zhǔn)備階段 "select#": 1, "steps": [ { "expanded_query": "/* select#1 */ select `employees`.`id` AS `id`,`employees`.`name` AS `name`,`employees`.`age` AS `age`,`employees`.`position` AS `position`,`employees`.`hire_time` AS `hire_time` from `employees` where (`employees`.`name` > 'a') order by `employees`.`position`" } ] /* steps */ } /* join_preparation */ }, { "join_optimization": { --第二階段:SQL優(yōu)化階段 "select#": 1, "steps": [ { "condition_processing": { --條件處理 "condition": "WHERE", "original_condition": "(`employees`.`name` > 'a')", "steps": [ { "transformation": "equality_propagation", "resulting_condition": "(`employees`.`name` > 'a')" }, { "transformation": "constant_propagation", "resulting_condition": "(`employees`.`name` > 'a')" }, { "transformation": "trivial_condition_removal", "resulting_condition": "(`employees`.`name` > 'a')" } ] /* steps */ } /* condition_processing */ }, { "substitute_generated_columns": { } /* substitute_generated_columns */ }, { "table_dependencies": [ --表依賴詳情 { "table": "`employees`", "row_may_be_null": false, "map_bit": 0, "depends_on_map_bits": [ ] /* depends_on_map_bits */ } ] /* table_dependencies */ }, { "ref_optimizer_key_uses": [ ] /* ref_optimizer_key_uses */ }, { "rows_estimation": [ --預(yù)估表的訪問(wèn)成本 { "table": "`employees`", "range_analysis": { "table_scan": { --全表掃描 "rows": 3, --掃描行數(shù) "cost": 3.7 --查詢成本 } /* table_scan */, "potential_range_indexes": [ --查詢可能使用的索引 { "index": "PRIMARY", --主鍵索引 "usable": false, "cause": "not_applicable" }, { "index": "idx_name_age_position", --輔助索引 "usable": true, "key_parts": [ "name", "age", "position", "id" ] /* key_parts */ }, { "index": "idx_age", "usable": false, "cause": "not_applicable" } ] /* potential_range_indexes */, "setup_range_conditions": [ ] /* setup_range_conditions */, "group_index_range": { "chosen": false, "cause": "not_group_by_or_distinct" } /* group_index_range */, "analyzing_range_alternatives": { --分析各個(gè)索引使用成本 "range_scan_alternatives": [ { "index": "idx_name_age_position", "ranges": [ "a < name" --索引使用范圍 ] /* ranges */, "index_pes_for_eq_ranges": true, "rowid_ordered": false, --使用該索引獲取的記錄是否按照主鍵排序 "using_mrr": false, "index_only": false, --是否使用覆蓋索引 "rows": 3, --索引掃描行數(shù) "cost": 4.61, --索引使用成本 "chosen": false, --是否選擇該索引 "cause": "cost" } ] /* range_scan_alternatives */, "analyzing_roworder_intersect": { "usable": false, "cause": "too_few_roworder_scans" } /* analyzing_roworder_intersect */ } /* analyzing_range_alternatives */ } /* range_analysis */ } ] /* rows_estimation */ }, { "considered_execution_plans": [ { "plan_prefix": [ ] /* plan_prefix */, "table": "`employees`", "best_access_path": { --最優(yōu)訪問(wèn)路徑 "considered_access_paths": [ --最終選擇的訪問(wèn)路徑 { "rows_to_scan": 3, "access_type": "scan", --訪問(wèn)類型:為sacn,全表掃描 "resulting_rows": 3, "cost": 1.6, "chosen": true, --確定選擇 "use_tmp_table": true } ] /* considered_access_paths */ } /* best_access_path */, "condition_filtering_pct": 100, "rows_for_plan": 3, "cost_for_plan": 1.6, "sort_cost": 3, "new_cost_for_plan": 4.6, "chosen": true } ] /* considered_execution_plans */ }, { "attaching_conditions_to_tables": { "original_condition": "(`employees`.`name` > 'a')", "attached_conditions_computation": [ ] /* attached_conditions_computation */, "attached_conditions_summary": [ { "table": "`employees`", "attached": "(`employees`.`name` > 'a')" } ] /* attached_conditions_summary */ } /* attaching_conditions_to_tables */ }, { "clause_processing": { "clause": "ORDER BY", "original_clause": "`employees`.`position`", "items": [ { "item": "`employees`.`position`" } ] /* items */, "resulting_clause_is_simple": true, "resulting_clause": "`employees`.`position`" } /* clause_processing */ }, { "reconsidering_access_paths_for_index_ordering": { "clause": "ORDER BY", "index_order_summary": { "table": "`employees`", "index_provides_order": false, "order_direction": "undefined", "index": "unknown", "plan_changed": false } /* index_order_summary */ } /* reconsidering_access_paths_for_index_ordering */ }, { "refine_plan": [ { "table": "`employees`" } ] /* refine_plan */ } ] /* steps */ } /* join_optimization */ }, { "join_execution": { --第三階段:SQL執(zhí)行階段 "select#": 1, "steps": [ { "filesort_information": [ { "direction": "asc", "table": "`employees`", "field": "position" } ] /* filesort_information */, "filesort_priority_queue_optimization": { "usable": false, "cause": "not applicable (no LIMIT)" } /* filesort_priority_queue_optimization */, "filesort_execution": [ ] /* filesort_execution */, "filesort_summary": { "rows": 3, "examined_rows": 3, "number_of_tmp_files": 0, "sort_buffer_size": 200704, "sort_mode": "<sort_key, packed_additional_fields>" } /* filesort_summary */ } ] /* steps */ } /* join_execution */ } ] /* steps */ }復(fù)制代碼
結(jié)論:全表掃描的成本低于索引掃描,所以MySQL最終選擇全表掃描。
案例2
select * from employees where name > 'zzz' order by position;SELECT * FROM information_schema.OPTIMIZER_TRACE; 復(fù)制代碼
結(jié)論:查看trace字段可知索引掃描的成本低于全表掃描,所以MySQL最終選擇索引掃描。
常見(jiàn)SQL深入優(yōu)化
Order by
與 Group by
優(yōu)化
案例1
EXPLAIN select * from employees where name = 'ZhangSan' and position = 'dev' order by age復(fù)制代碼

分析:
利用最左前綴法則:中間字段不能斷,因此查詢用到了 name索引
,從 key_len = 74 也能看出,age 索引列用在排序過(guò)程過(guò)程中,因?yàn)?Extra 字段里沒(méi)有 using filesort
。
案例2
EXPLAIN select * from employees where name = 'ZhangSan' order by position復(fù)制代碼

分析:
從 explain 的執(zhí)行結(jié)果來(lái)看:key_len = 74,查詢使用了 name 索引,由于用了 position 進(jìn)行排序,跳過(guò)了 age,出現(xiàn)了 Using filesort
。
案例3
EXPLAIN select * from employees where name = 'ZhangSan' order by age,position復(fù)制代碼

分析:
查詢只用到索引name
,age 和 position 用于排序,無(wú)Using filesort
。
案例4
EXPLAIN select * from employees where name = 'ZhangSan' order by position,age復(fù)制代碼

分析:
和案例3中explain的執(zhí)行結(jié)果一樣,但是出現(xiàn)了Using filesort
,因?yàn)樗饕膭?chuàng)建順序?yàn)?name,age,position
, 但是排序的時(shí)候 age 和 position 顛倒位置了。
案例5
EXPLAIN select * from employees where name = 'ZhangSan' and age = 18 order by position,age復(fù)制代碼

分析:
與案例4對(duì)比,在Extra中并未出現(xiàn)** Using filesort
**,因?yàn)?age 為常量,在排序中被優(yōu)化,所以索引未顛倒,不會(huì)出現(xiàn) Using filesort
。
案例6
EXPLAIN select * from employees where name = 'ZhangSan' order by age asc, position desc;復(fù)制代碼

分析:
雖然排序的字段列與索引順序一樣,且 order by
默認(rèn)升序,這里 position desc
變成列降序,導(dǎo)致與索引的排序方式不同,從而產(chǎn)生 Using filesort
。MySQL8 以上版本有降序索引可以支持該種查詢方式。
案例7
EXPLAIN select * from employees where name in ('ZhangSan', 'hjh') order by age, position;復(fù)制代碼
分析:
對(duì)于排序來(lái)說(shuō),多個(gè)相等條件也是范圍查詢。
案例8
EXPLAIN select * from employees where name > 'a' order by name;復(fù)制代碼

可以用覆蓋索引優(yōu)化
EXPLAIN select name,age,position from employees where name > 'a' order by name;復(fù)制代碼

優(yōu)化總結(jié)
- MySQL支持兩種方式的排序
filesort
和index
。Using index 是指MySQL 掃描索引本身完成排序。index 效率高,filesort 效率低。 - order by 滿足兩種情況會(huì)使用 Using index.
- order by 語(yǔ)句使用索引最左前例。
- 使用 where 子句與 order by 子句條件列組合滿足索引最左前例。
- 盡量在索引列上完成排序,遵循索引建立(索引創(chuàng)建的順序)時(shí)的最左前綴法則。
- 如果 order by 的條件不在索引列上,就會(huì)產(chǎn)生 Using filesort。
- 能用覆蓋索引盡量用覆蓋索引。
- group by 和 order by 很類似,其實(shí)質(zhì)是先排序后分組,遵循索引創(chuàng)建順序的最左前綴法則。對(duì)于 group by 的優(yōu)化如果不需要排序的可以加上
order by null
禁止排序。注意:where 高于 having,能寫在 where 中的限定條件就不要去 having 限定了。
Using filesort文件排序原理
filesort文件排序方式
- 單路排序:是一次性取出滿足條件行的所有字段,然后在
sort buffer
中進(jìn)行排序;用 trace 工具可以看到 sort_mode 信息里顯示 < sort_key, additional_fields > 或者 < sort_key, packed_additional_fields >。 - 雙路排序(又叫回表排序模式):是首先根據(jù)相應(yīng)的條件取出相應(yīng)的排序字段和可以直接定位運(yùn)行數(shù)據(jù)的行ID,然后在 sort buffer 中進(jìn)行排序,排序完后需要再次取回其它需要的字段;用 trace 工具可以看到 sort_mode 信息里顯示 < sort_key, rowid >
MySQL 通過(guò)比較系統(tǒng)變量 max_length_for_sort_data
(默認(rèn)1024字節(jié)) 的大小和需要查詢的字段總大小來(lái)判斷使用那種排序模式。
- 如果
max_length_for_sort_data
比查詢的字段的總長(zhǎng)度大,那么使用單路排序模式; - 如果
max_length_for_sort_data
比查詢字段的總長(zhǎng)度小,那么使用雙路排序模式。
驗(yàn)證各種排序方式
EXPLAIN select * from employees where name = 'ZhangSan' order by position;復(fù)制代碼

查看下這條sql對(duì)應(yīng)trace結(jié)果如下(只展示排序部分):
set session optimizer_trace="enabled=on",end_markers_in_json=on; #開(kāi)啟traceselect * from employees where name = 'ZhangSan' order by position;select * from information_schema.OPTIMIZER_TRACE;復(fù)制代碼
"join_execution": { --SQL執(zhí)行階段 "select#": 1, "steps": [ { "filesort_information": [ { "direction": "asc", "table": "`employees`", "field": "position" } ] /* filesort_information */, "filesort_priority_queue_optimization": { "usable": false, "cause": "not applicable (no LIMIT)" } /* filesort_priority_queue_optimization */, "filesort_execution": [ ] /* filesort_execution */, "filesort_summary": { --文件排序信息 "rows": 1, --預(yù)計(jì)掃描行數(shù) "examined_rows": 1, --參數(shù)排序的行 "number_of_tmp_files": 0, --使用臨時(shí)文件的個(gè)數(shù),這個(gè)只如果為0代表全部使用的sort_buffer內(nèi)存排序,否則使用的磁盤文件排序 "sort_buffer_size": 200704, --排序緩存的大小 "sort_mode": "<sort_key, packed_additional_fields>" --排序方式,這里用的單路排序 } /* filesort_summary */ } ] /* steps */ } /* join_execution */復(fù)制代碼
修改系統(tǒng)變量 max_length_for_sort_data
(默認(rèn)1024字節(jié)) ,employees 表所有字段長(zhǎng)度總和肯定大于10字節(jié)
set max_length_for_sort_data = 10; select * from employees where name = 'ZhangSan' order by position;select * from information_schema.OPTIMIZER_TRACE;復(fù)制代碼
trace排序部分結(jié)果:
"join_execution": { "select#": 1, "steps": [ { "filesort_information": [ { "direction": "asc", "table": "`employees`", "field": "position" } ] /* filesort_information */, "filesort_priority_queue_optimization": { "usable": false, "cause": "not applicable (no LIMIT)" } /* filesort_priority_queue_optimization */, "filesort_execution": [ ] /* filesort_execution */, "filesort_summary": { "rows": 1, "examined_rows": 1, "number_of_tmp_files": 0, "sort_buffer_size": 53248, "sort_mode": "<sort_key, rowid>" --排序方式,這里用餓的雙路排序 } /* filesort_summary */ } ] /* steps */ } /* join_execution */ 復(fù)制代碼
單路排序的詳細(xì)過(guò)程:
- 從索引 name 找到第一個(gè)滿足 name='ZhangSan' 條件的主鍵 id;
- 根據(jù)主鍵id取出整行,取出所有字段的值,存入sort_buffer中;
- 從索引name找到下一個(gè)滿足 name='ZhangSan' 條件的主鍵 id;
- 重復(fù)步驟2、3直到不滿足 name='ZhangSan';
- 對(duì) sort_buffer 中的數(shù)據(jù)按照字段 position 進(jìn)行排序;
- 返回結(jié)果給客戶端
雙路排序的詳細(xì)過(guò)程:
- 從索引 name 找到第一個(gè)滿足 name='ZhangSan' 的主鍵id;
- 根據(jù)主鍵id取出整行,把排序字段 position 和 主鍵id 這兩個(gè)字段放到 sort_buffer 中;
- 從索引 name 取下一個(gè)滿足 name='ZhangSan' 記錄的主鍵id;
- 重復(fù)步驟3、4直到不滿足 name='ZhangSan';
- 對(duì) sort_buffer 中的字段 position 和 主鍵id按照 position 進(jìn)行排序;
- 遍歷排序好的 id 和 字段 position,按照 id 的值回到原表中取出所有的字段的值返回給客戶端。
對(duì)比兩個(gè)排序模式,單路排序會(huì)把所有需要查詢的字段都放到 sort_buffer 中,而雙路排序只會(huì)把主鍵和需要排序的字段放到 sort_buffer 中進(jìn)行排序,然后再通過(guò)主鍵回到原表查詢需要的字段。
如果MySQL排序內(nèi)存配置的比較小并且沒(méi)有條件繼續(xù)增加了,可以適當(dāng)把 max_length_for_sort_data
配置小點(diǎn),讓優(yōu)化器選擇使用雙路排序算法,可以在 sort_buffer 中一次排序