MySQL 索引優(yōu)化案例
數(shù)據(jù)準(zhǔn)備
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=1 DEFAULT CHARSET=utf8 COMMENT='員工記錄表';
INSERT INTO employees(name,age,position,hire_time) VALUES('LiLei',22,'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());
‐‐ 插入一些示例數(shù)據(jù)
drop procedure if exists insert_emp;
delimiter ;;
create procedure insert_emp()
begin
declare i int;
set i=1;
while(i<=100000)do
insert into employees(name,age,position) values(CONCAT('zhuge',i),i,'dev');
set i=i+1;
end while;
end;;
delimiter ;
call insert_emp();如上有 employees 表,有主鍵索引和 (name, age, position ) 聯(lián)合索引, 看下面的查詢示例:
聯(lián)合索引的首字段用范圍查詢
EXPLAIN SELECT * FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

結(jié)論:聯(lián)合索引第一個(gè)字段就用范圍查找不會(huì)走索引,mysql內(nèi)部可能覺(jué)得第一個(gè)字段就用范圍,結(jié)果集應(yīng)該很大,回表效率不高,還不如就全表掃描
強(qiáng)制走索引
EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei' AND age = 22 AND position ='manager';

結(jié)論:雖然使用了強(qiáng)制走索引讓聯(lián)合索引第一個(gè)字段范圍查找也走索引,掃描的行rows看上去也少了點(diǎn),但是最終查找效率不一定比全表掃描高,因?yàn)榛乇硇什桓撸?一般不會(huì)使用這個(gè)手段,除非有證據(jù)能證明強(qiáng)制走索引后效率大幅度提高
覆蓋索引優(yōu)化
EXPLAIN SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

將 select * 修改為 select name, age, posiion , 只選擇索引中已經(jīng)存在的列,可以不用回表,所以會(huì)利用索引
in和or什么時(shí)候會(huì)走索引
在表數(shù)據(jù)量比較大的情況會(huì)走索引,數(shù)據(jù)量不多的情況下會(huì)選擇全表掃描,示例如下:
in 查詢
EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
用到全部索引 or 查詢
EXPLAIN SELECT * FROM employees WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position ='manager';

用到全部索引
下面新建一張 employees_copy 表,結(jié)構(gòu)和 employee 一樣,但數(shù)據(jù)只有三條, 再執(zhí)行上面兩個(gè)查詢
in 查詢
EXPLAIN SELECT * FROM employees_copy WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
全表掃描
or查詢
EXPLAIN SELECT * FROM employees_copy WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position ='manager';

全表掃描
like xx% 一般都會(huì)走索引,和數(shù)據(jù)量無(wú)關(guān)
大表
EXPLAIN SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager';

小表
EXPLAIN SELECT * FROM employees_copy WHERE name like 'LiLei%' AND age = 22 AND position ='manager';

可以看到,無(wú)論表的數(shù)據(jù)量大小,都會(huì)利用索引,為什么呢?
其實(shí) like 用到了索引下推的優(yōu)化
索引下推
對(duì)于輔助聯(lián)合索引,正常情況下按照最左前綴原則, SELECT * from employees where name like 'LiLei%' and age = 22 and position = 'dev' 這種情況下只會(huì)走name字段的索引,因?yàn)楦鶕?jù)name字段過(guò)濾完,得到的索引行里的age和position是無(wú)序的,無(wú)法很好的利用索引。
在MySQL5.6之前的版本,這個(gè)查詢只能在聯(lián)合索引里匹配到名字是 'LiLei' 開(kāi)頭的索引,然后拿這些索引對(duì)應(yīng)的主鍵逐個(gè)回表,到主鍵索引上找出相應(yīng)的記錄,再比對(duì)age和position這兩個(gè)字段的值是否符合。
MySQL 5.6引入了索引下推優(yōu)化,可以在索引遍歷過(guò)程中,對(duì)索引中包含的所有字段先做判斷,過(guò)濾掉不符合條件的記錄之后再回表,可以有效的減少回表次數(shù)。使用了索引下推優(yōu)化后,上面那個(gè)查詢?cè)诼?lián)合索引里匹配到名字是 'LiLei' 開(kāi)頭的索引之后,同時(shí)還會(huì)在索引里過(guò)濾age和position這兩個(gè)字段,拿著過(guò)濾完剩下的索引對(duì)應(yīng)的主鍵id再回表查整行數(shù)據(jù)。
索引下推會(huì)減少回表次數(shù),對(duì)于innodb引擎的表索引下推只能用于二級(jí)索引,innodb的主鍵索引(聚簇索引)樹(shù)葉子節(jié)點(diǎn)上保存的是全行數(shù)據(jù),所以這個(gè)時(shí)候索引下推并不會(huì)起到減少查詢?nèi)袛?shù)據(jù)的效果。
為什么范圍查找沒(méi)有用索引下推優(yōu)化?
估計(jì)應(yīng)該是Mysql認(rèn)為范圍查找過(guò)濾的結(jié)果集過(guò)大,like KK% 在絕大多數(shù)情況來(lái)看,過(guò)濾后的結(jié)果集比較小,所以這里Mysql選擇給 like KK% 用了索引下推優(yōu)化,當(dāng)然這也不是絕對(duì)的,有時(shí)like KK% 也不一定就會(huì)走索引下推。
如何選擇索引
先看下面的兩個(gè)查詢:


同樣的表,同樣的字段,因?yàn)闂l件的不同,選擇的索引也不同,MySQL 是如何選擇的呢?
Trace 工具
MySQl 提供了一個(gè)工具,可以看到選擇索引的計(jì)算過(guò)程, 用法如下:
mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on; --開(kāi)啟trace mysql> select * from employees where name > 'a' order by position; mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;
下面是對(duì) trace 字段的解析
{
"steps": [
{
"join_preparation": { //第一階段:SQL準(zhǔn)備階段,格式化sql
"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": 10123, //掃描行數(shù)
"cost": 2054.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 */
}
] /* 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_dives_for_eq_ranges": true,
"rowid_ordered": false, //使用該索引獲取的記錄是否按照主鍵排序
"using_mrr": false,
"index_only": false, //是否使用覆蓋索引
"rows": 5061, //索引掃描行數(shù)
"cost": 6074.2, //索引使用成本
"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": 10123,
"access_type": "scan", //訪問(wèn)類型:為scan,全表掃描
"resulting_rows": 10123,
"cost": 2052.6,
"chosen": true, //確定選擇
"use_tmp_table": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 10123,
"cost_for_plan": 2052.6,
"sort_cost": 10123,
"new_cost_for_plan": 12176,
"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",
"steps": [
] /* steps */,
"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": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
// 結(jié)論:全表掃描的成本低于索引掃描,所以mysql最終選擇全表掃描mysql> select * from employees where name > 'zzz' order by position; mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE; # 查看trace字段可知索引掃描的成本低于全表掃描,所以mysql最終選擇索引掃描 mysql> set session optimizer_trace="enabled=off"; //關(guān)閉trace
深入優(yōu)化
order by 和 group by
order by 和 group by 也會(huì)遵循左前綴法則, 如下例子 :

根據(jù)左前綴法則,用到了 name 字段的索引,同時(shí)使用 age 字段用來(lái)排序, 因?yàn)?extra 種沒(méi)有 filesort
order by 或者 group by 用到的索引不會(huì)參與到 key_len 的計(jì)算,索引 key_len 仍然只是 74, 即 name字段的長(zhǎng)度
再看下面一個(gè)例子:

where 條件是name 排序字段是 position 跳過(guò)了age字段,所以只能用 name 索引,無(wú)法利用 position 索引進(jìn)行索引排序,用到是文件排序
再看第三個(gè)例子:

使用name條件查詢, 同時(shí)使用 age position 雙字段排序,沒(méi)有跳過(guò)聯(lián)合索引的字段. 所以可以用索引排序
然后顛倒一下排序順序,先position 再 age:

發(fā)現(xiàn)此時(shí)只能文件排序了
再看下面的例子

雖然排序字段與索引字段不一樣,但仍然是索引排序, 因?yàn)椴樵儣l件中 用到是 (name, age)索引,排序中用到是 position 索引,并沒(méi)有顛倒順序。所以還是索引排序
如果一個(gè)正序一個(gè)倒序呢?

雖然排序字段與索引字段順序相同, 但是 age 是正序, position 是倒敘,導(dǎo)致與索引的排序方式不同,無(wú)法利用索引。從而發(fā)生了文件排序, Mysql8以上版本有降序索引可以支持該種查詢方式。
先 in 查詢:

對(duì)于排序來(lái)說(shuō),多個(gè)相等條件也是范圍查詢, 無(wú)法利用索引排序
先范圍查詢:

這里發(fā)生了全表掃描,沒(méi)有任何索引,排序自然也無(wú)法利用索引了,可以使用覆蓋索引優(yōu)化:

優(yōu)化總結(jié)
- MySQL支持兩種方式的排序filesort和index,Using index是指MySQL掃描索引本身完成排序。index效率高,filesort效率低。
2、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,能寫(xiě)在where中的限定條件就不要去having限定了。
Using filesort文件排序原理詳解
單路排序模式;
是一次性取出滿足條件行的所有字段,然后在sort buffer中進(jìn)行排序;用trace工具可以看到sort_mode信息里顯示< sort_key, additional_fields >或者< sort_key, packed_additional_fields >
雙路排序(又叫回表排序模式)
是首先根據(jù)相應(yīng)的條件取出相應(yīng)的排序字段和可以直接定位行數(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)判斷使用哪種排序模式。
- 如果 字段的總長(zhǎng)度小于max_length_for_sort_data ,那么使用 單路排序模式;
- 如果 字段的總長(zhǎng)度大于max_length_for_sort_data ,那么使用 雙路排序模·式。
分頁(yè)優(yōu)化
常規(guī)的limit分頁(yè)
有如下查詢語(yǔ)句
select * from employees limit 10000,10;
該sql并不是只查詢了10條,而是查找了10010條,然后把前10000條結(jié)果給舍棄掉, 因此要查詢一個(gè)大表靠后的內(nèi)容,執(zhí)行效率是非常低的
優(yōu)化
根據(jù)主鍵排序
上面的下面的sql語(yǔ)句沒(méi)有指定排序方式,默認(rèn)使用ID排序。當(dāng)使用ID排序時(shí),我們可以使用下面的優(yōu)化。
select * from employees where id > 90000 limit 5;
如果id是連續(xù)自增的,和limit 90000,5 結(jié)果沒(méi)有差別,是 90001 ~ 90005 的數(shù)據(jù)。
但是如果在90000之前刪除了一條數(shù)據(jù),結(jié)果就不一樣了,id > 90000 limit 5 的結(jié)果是 90001 ~ 90005, 但是limit 90000, 5 的結(jié)果是 90002 ~ 90006, 很明顯 90002 ~ 90006 才是符合我們直覺(jué)的。所以這個(gè)優(yōu)化只能限制與排序條件是連續(xù)的。如果id不是自增的呢?會(huì)出現(xiàn)什么情況,假如 90000 這條數(shù)據(jù)有兩個(gè),limit 90000, 5 的結(jié)果是 90000 ~ 90004,而 id > 90000 limit 5 的結(jié)果仍是 90001 ~ 90005, 會(huì)把 id= 90000 的數(shù)據(jù)漏掉一條。
所以這個(gè)優(yōu)化只能用于排序的字段是連續(xù)自增的,并且不能重復(fù)
非主鍵排序的優(yōu)化
有如下查詢語(yǔ)句
EXPLAIN select * from employees ORDER BY name limit 90000,5;

發(fā)現(xiàn)并沒(méi)有用上name的索引,因?yàn)?nbsp;select * ,掃描聯(lián)合索引時(shí),無(wú)法的到全部數(shù)據(jù),需要回表,成本比全表掃描更高,所以優(yōu)化器放棄使用索引。
可以使用索引覆蓋的方法,使用分頁(yè)查詢僅僅找到少量的主鍵,然后在使用主鍵查找整行數(shù)據(jù), 如下:
select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;
看下執(zhí)行計(jì)劃:

原 SQL 使用文件排序,優(yōu)化后的使用索引排序
表關(guān)聯(lián)優(yōu)化
先造一些數(shù)據(jù):
CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_a` (`a`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; create table t2 like t1; -- 插入一些示例數(shù)據(jù) -- 往t1表插入1萬(wàn)行記錄 drop?procedure?if?exists?insert_t1;? delimiter?;; create?procedure?insert_t1() begin ??declare?i?int; ??set?i=1; ??while(i<=10000)do ????insert?into?t1(a,b)?values(i,i);?? ????set?i=i+1; ??end?while; end;; delimiter?; call?insert_t1(); -- 往t2表插入100行記錄 drop?procedure?if?exists?insert_t2;? delimiter?;; create?procedure?insert_t2() begin ??declare?i?int; ??set?i=1; ??while(i<=100)do ????insert?into?t2(a,b)?values(i,i);?? ????set?i=i+1; ??end?while; end;; delimiter?; call?insert_t2();
新建 t1 t2 表,結(jié)構(gòu)一樣, 都在a字段上有索引,b字段沒(méi)有索引,t1表有 10000 行記錄,t2表只有100條記錄。
常見(jiàn)的表關(guān)聯(lián)算法
- 內(nèi)嵌循環(huán)連接算法 Nested-Loop Join
- 基于塊的嵌套循環(huán)連接算法 Block Nested-Loop Join
內(nèi)嵌循環(huán)連接算法
一次一行循環(huán)地從第一張表(稱為驅(qū)動(dòng)表)中讀取行,在這行數(shù)據(jù)中取到關(guān)聯(lián)字段,根據(jù)關(guān)聯(lián)字段在另一張表(被驅(qū)動(dòng)表)里取出滿足條件的行,然后取出兩張表的結(jié)果合集。
一般關(guān)聯(lián)字段有索引的時(shí)候使用這種算法, 示例:
EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;

從執(zhí)行計(jì)劃中可以看到這些信息:
- 驅(qū)動(dòng)表是 t2,被驅(qū)動(dòng)表是 t1。先執(zhí)行的就是驅(qū)動(dòng)表;優(yōu)化器一般會(huì)優(yōu)先選擇小表做驅(qū)動(dòng)表,用where條件過(guò)濾完驅(qū)動(dòng)表,然后再跟被驅(qū)動(dòng)表做關(guān)聯(lián)查詢。所以使用 inner join 時(shí),排在前面的表并不一定就是驅(qū)動(dòng)表
- 當(dāng)使用left join時(shí),左表是驅(qū)動(dòng)表,右表是被驅(qū)動(dòng)表,當(dāng)使用right join時(shí),右表時(shí)驅(qū)動(dòng)表,左表是被驅(qū)動(dòng)表
- 使用了 NLJ算法。一般 join 語(yǔ)句中,如果執(zhí)行計(jì)劃 Extra 中未出現(xiàn) Using join buffer 則表示使用的 join 算法是 NLJ。
上面sql的大致流程如下:
- 從表 t2 中讀取一行數(shù)據(jù)(如果t2表有查詢過(guò)濾條件的,用先用條件過(guò)濾完,再?gòu)倪^(guò)濾結(jié)果里取出一行數(shù)據(jù));
- 從第 1 步的數(shù)據(jù)中,取出關(guān)聯(lián)字段 a,到表 t1 中查找;
- 取出表 t1 中滿足條件的行,跟 t2 中獲取到的結(jié)果合并,作為結(jié)果返回給客戶端;
- 重復(fù)上面 3 步。
整個(gè)過(guò)程會(huì)讀取 t2 表的所有數(shù)據(jù)(掃描100行),然后遍歷這每行數(shù)據(jù)中字段 a 的值,根據(jù) t2 表中 a 的值索引掃描 t1 表中的對(duì)應(yīng)行(掃描100次 t1 表的索引,1次掃描可以認(rèn)為最終只掃描 t1 表一行完整數(shù)據(jù),也就是總共 t1 表也掃描了100行)。因此整個(gè)過(guò)程掃描了 200 行
基于塊的嵌套循環(huán)算法
當(dāng)關(guān)聯(lián)字段沒(méi)有沒(méi)有索引的時(shí)候會(huì)使用這種算法
把驅(qū)動(dòng)表的數(shù)據(jù)讀入到 join_buffer 中,然后掃描被驅(qū)動(dòng)表,把被驅(qū)動(dòng)表每一行取出來(lái)跟 join_buffer 中的數(shù)據(jù)做對(duì)比。
如下:
EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;

Extra 中 的Using join buffer (Block Nested Loop)說(shuō)明該關(guān)聯(lián)查詢使用的是 BNL 算法。
上面sql的大致流程如下:
- 把 t2 的所有數(shù)據(jù)放入到 join_buffer 中
- 把表 t1 中每一行取出來(lái),跟 join_buffer 中的數(shù)據(jù)做對(duì)比
- 返回滿足 join 條件的數(shù)據(jù)
整個(gè)過(guò)程對(duì)表 t1 和 t2 都做了一次全表掃描,因此掃描的總行數(shù)為10000(表 t1 的數(shù)據(jù)總量) + 100(表 t2 的數(shù)據(jù)總量) = 10100。并且 join_buffer 里的數(shù)據(jù)是無(wú)序的,因此對(duì)表 t1 中的每一行,都要做 100 次判斷,所以內(nèi)存中的判斷次數(shù)是 100 * 10000= 100 萬(wàn)次。
這個(gè)例子里表 t2 才 100 行,要是表 t2 是一個(gè)大表,join_buffer 放不下怎么辦呢?·
join_buffer 的大小是由參數(shù) join_buffer_size 設(shè)定的,默認(rèn)值是 256k。如果放不下表 t2 的所有數(shù)據(jù)話,策略很簡(jiǎn)單,就是分段放。
比如 t2 表有1000行記錄, join_buffer 一次只能放800行數(shù)據(jù),那么執(zhí)行過(guò)程就是先往 join_buffer 里放800行記錄,然后從 t1 表里取數(shù)據(jù)跟 join_buffer 中數(shù)據(jù)對(duì)比得到部分結(jié)果,然后清空 join_buffer ,再放入 t2 表剩余200行記錄,再次從 t1 表里取數(shù)據(jù)跟 join_buffer 中數(shù)據(jù)對(duì)比。所以就多掃了一次 t1 表。
為什么要使用 BNLJ 算法呢?
如果上面第二條sql使用 Nested-Loop Join,那么掃描行數(shù)為 100 * 10000 = 100萬(wàn)次,這個(gè)是磁盤(pán)掃描。
很顯然,用BNL磁盤(pán)掃描次數(shù)少很多,相比于磁盤(pán)掃描,BNL的內(nèi)存計(jì)算會(huì)快得多。
因此MySQL對(duì)于被驅(qū)動(dòng)表的關(guān)聯(lián)字段沒(méi)索引的關(guān)聯(lián)查詢,一般都會(huì)使用 BNL 算法。如果有索引一般選擇 NLJ 算法,有索引的情況下 NLJ 算法比 BNL算法性能更高
對(duì)于關(guān)聯(lián)sql的優(yōu)化
- 關(guān)聯(lián)字段加索引,讓mysql做join操作時(shí)盡量選擇NLJ算法,驅(qū)動(dòng)表因?yàn)樾枰坎樵兂鰜?lái),所以過(guò)濾的條件也盡量要走索引,避免全表掃描,總之,能走索引的過(guò)濾條件盡量都走索引
- 小表驅(qū)動(dòng)大表,寫(xiě)多表連接sql時(shí)如果明確知道哪張表是小表可以用
straight_join寫(xiě)法固定連接驅(qū)動(dòng)方式,省去mysql優(yōu)化器自己判斷的時(shí)間
straight_join解釋:straight_join功能同join類似,但能讓左邊的表來(lái)驅(qū)動(dòng)右邊的表,能改表優(yōu)化器對(duì)于聯(lián)表查詢的執(zhí)行順序。
比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql選著 t2 表作為驅(qū)動(dòng)表。
- straight_join只適用于inner join,并不適用于left join,right join。(因?yàn)閘eft join,right join已經(jīng)代表指定了表的執(zhí)行順序)
- 盡可能讓優(yōu)化器去判斷,因?yàn)榇蟛糠智闆r下mysql優(yōu)化器是比人要聰明的。使用straight_join一定要慎重,因?yàn)椴糠智闆r下人為指定的執(zhí)行順序并不一定會(huì)比優(yōu)化引擎要靠譜。
小表的定義
在決定哪個(gè)表做驅(qū)動(dòng)表的時(shí)候,應(yīng)該是兩個(gè)表按照各自的條件過(guò)濾,過(guò)濾完成之后,計(jì)算參與 join 的各個(gè)字段的總數(shù)據(jù)量,數(shù)據(jù)量小的那個(gè)表,就是“小表”,應(yīng)該作為驅(qū)動(dòng)表。不單單是表的總數(shù)據(jù)量
in和exsits優(yōu)化
原則:小表驅(qū)動(dòng)大表,即小的數(shù)據(jù)集驅(qū)動(dòng)大的數(shù)據(jù)集
- in:當(dāng)B表的數(shù)據(jù)集小于A表的數(shù)據(jù)集時(shí),in優(yōu)于exists
select * from A where id in (select id from B)
#等價(jià)于:
for(select id from B){
select * from A where A.id = B.id
}- exists:當(dāng)A表的數(shù)據(jù)集小于B表的數(shù)據(jù)集時(shí),exists優(yōu)于in
將主查詢A的數(shù)據(jù),放到子查詢B中做條件驗(yàn)證,根據(jù)驗(yàn)證結(jié)果(true或false)來(lái)決定主查詢的數(shù)據(jù)是否保留
select * from A where exists (select 1 from B where B.id = A.id)
#等價(jià)于:
for(select * from A){
select * from B where B.id = A.id
}
#A表與B表的ID字段應(yīng)建立索引關(guān)于Exists
- EXISTS (subquery)只返回TRUE或FALSE,因此子查詢中的SELECT * 也可以用SELECT 1替換,官方說(shuō)法是實(shí)際執(zhí)行時(shí)會(huì)忽略SELECT清單,因此沒(méi)有區(qū)別
- EXISTS子查詢的實(shí)際執(zhí)行過(guò)程可能經(jīng)過(guò)了優(yōu)化而不是我們理解上的逐條對(duì)比
- EXISTS子查詢往往也可以用JOIN來(lái)代替,何種最優(yōu)需要具體問(wèn)題具體分析
count 查詢優(yōu)化
有下面四條查詢語(yǔ)句:
EXPLAIN select count(1) from employees; EXPLAIN select count(id) from employees; EXPLAIN select count(name) from employees; EXPLAIN select count(*) from employees;
只有 count(字段名) 不會(huì)把該字段為null 計(jì)入總數(shù)
其實(shí)上面四條的查詢計(jì)劃都一樣,效率上沒(méi)有太大的差別

當(dāng)字段有索引
count(*)≈count(1)>count(字段)>count(主鍵 id)
字段有索引,count(字段)統(tǒng)計(jì)走二級(jí)索引,二級(jí)索引存儲(chǔ)數(shù)據(jù)比主鍵索引少,所以count(字段)>count(主鍵 id)
當(dāng)字段沒(méi)有索引
count(*)≈count(1)>count(主鍵 id)>count(字段)
字段沒(méi)有索引count(字段)統(tǒng)計(jì)走不了索引,count(主鍵 id)還可以走主鍵索引,所以count(主鍵 id)>count(字段)
count(1)
count(1)跟count(字段)執(zhí)行過(guò)程類似,不過(guò)count(1)不需要取出字段統(tǒng)計(jì),就用常量1做統(tǒng)計(jì),count(字段)還需要取出字段,所以理論上count(1)比count(字段)會(huì)快一點(diǎn)。
count(*)
count(*) 是例外,mysql并不會(huì)把全部字段取出來(lái),而是專門(mén)做了優(yōu)化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)來(lái)替代 count(*)。
為什么對(duì)于count(id),mysql最終選擇輔助索引而不是主鍵聚集索引?因?yàn)槎?jí)索引相對(duì)主鍵索引存儲(chǔ)數(shù)據(jù)更少,檢索性能應(yīng)該更高,mysql內(nèi)部做了點(diǎn)優(yōu)化(應(yīng)該是在5.7版本才優(yōu)化)。
常見(jiàn)優(yōu)化方法
自己維護(hù)的總行數(shù)
show table status 如果只需要知道表總行數(shù)的估計(jì)值可以用如下sql查詢,性能很高 show table status like 'employee'

將總數(shù)維護(hù)到Redis里 插入或刪除表數(shù)據(jù)行的時(shí)候同時(shí)維護(hù)redis里的表總行數(shù)key的計(jì)數(shù)值(用incr或decr命令),但是這種方式可能不準(zhǔn),很難保證表操作和redis操作的事務(wù)一致性
索引設(shè)計(jì)原則
索引設(shè)計(jì)原則:
1、代碼先行,索引后上
等到主體業(yè)務(wù)功能開(kāi)發(fā)完畢,把涉及到該表相關(guān)sql都要拿出來(lái)分析之后再建立索引。
2、聯(lián)合索引盡量覆蓋條件
比如可以設(shè)計(jì)一個(gè)或者兩三個(gè)聯(lián)合索引(盡量少建單值索引),讓每一個(gè)聯(lián)合索引都盡量去包含sql語(yǔ)句里的where、order by、group by的字段,還要確保這些聯(lián)合索引的字段順序盡量滿足sql查詢的最左前綴原則。
3、不要在小基數(shù)字段上建立索引
索引基數(shù)是指這個(gè)字段在表里總共有多少個(gè)不同的值,比如一張表總共100萬(wàn)行記錄,其中有個(gè)性別字段,其值不是男就是女,那么該字段的基數(shù)就是2。
如果對(duì)這種小基數(shù)字段建立索引的話,還不如全表掃描了,因?yàn)槟愕乃饕龢?shù)里就包含男和女兩種值,根本沒(méi)法進(jìn)行快速的二分查找,那用索引就沒(méi)有太大的意義了。
一般建立索引,盡量使用那些基數(shù)比較大的字段,就是值比較多的字段,那么才能發(fā)揮出B+樹(shù)快速二分查找的優(yōu)勢(shì)來(lái)。
4、長(zhǎng)字符串我們可以采用前綴索引
盡量對(duì)字段類型較小的列設(shè)計(jì)索引,比如說(shuō)什么tinyint之類的,因?yàn)樽侄晤愋洼^小的話,占用磁盤(pán)空間也會(huì)比較小,此時(shí)你在搜索的時(shí)候性能也會(huì)比較好一點(diǎn)。
當(dāng)然,這個(gè)所謂的字段類型小一點(diǎn)的列,也不是絕對(duì)的,很多時(shí)候你就是要針對(duì)varchar(255)這種字段建立索引,哪怕多占用一些磁盤(pán)空間也是有必要的。
對(duì)于這種varchar(255)的大字段可能會(huì)比較占用磁盤(pán)空間,可以稍微優(yōu)化下,比如針對(duì)這個(gè)字段的前20個(gè)字符建立索引,就是說(shuō),對(duì)這個(gè)字段里的每個(gè)值的前20個(gè)字符放在索引樹(shù)里,類似于 KEY index(name(20),age,position)
此時(shí)你在where條件里搜索的時(shí)候,如果是根據(jù)name字段來(lái)搜索,那么此時(shí)就會(huì)先到索引樹(shù)里根據(jù)name字段的前20個(gè)字符去搜索,定位到之后前20個(gè)字符的前綴匹配的部分?jǐn)?shù)據(jù)之后,再回到聚簇索引提取出來(lái)完整的name字段值進(jìn)行比對(duì)。
但是假如你要是order by name,那么此時(shí)你的name因?yàn)樵谒饕龢?shù)里僅僅包含了前20個(gè)字符,所以這個(gè)排序是沒(méi)法用上索引的, group by也是同理
5、where與order by沖突時(shí)優(yōu)先where
在where和order by出現(xiàn)索引設(shè)計(jì)沖突時(shí),到底是針對(duì)where去設(shè)計(jì)索引,還是針對(duì)order by設(shè)計(jì)索引?到底是讓where去用上索引,還是讓order by用上索引?
一般這種時(shí)候往往都是讓where條件去使用索引來(lái)快速篩選出來(lái)一部分指定的數(shù)據(jù),接著再進(jìn)行排序。
因?yàn)榇蠖鄶?shù)情況基于索引進(jìn)行where篩選往往可以最快速度篩選出你要的少部分?jǐn)?shù)據(jù),然后做排序的成本可能會(huì)小很多。
舉個(gè)例子
有 employees表,name, age, sex, position 列, 有聯(lián)合索引 (name, age, sex, position),
sex : 性別,取值0 或1
有如下查詢: select id from employees where name = 'zhangsan' and age = 18 and position = 'dev' 因?yàn)樘^(guò)了 sex 字段,position 無(wú)法利用索引
因?yàn)?sex 只有兩個(gè)取值,我們?cè)诓樵冋Z(yǔ)句上把 sex 的值全部枚舉出來(lái), 如下:
select id from employees where name = 'zhangsan' and age = 18 and sex in (0, 1) and position = 'dev'
這樣一來(lái)就可以利用全部索引了。
再舉個(gè)例子
加入我們要查詢最近一周登錄的用戶,首先想到的是 last_login_time > {一周之前的時(shí)間}
這是一個(gè)范圍查詢,在后面的所有字段便無(wú)法利用索引了,我們可以再設(shè)計(jì)一個(gè)字段,recent_login_flag(tinyint) 標(biāo)識(shí)是否最近登錄過(guò)。用定時(shí)任務(wù)定期更新該字段的值。這樣就由范圍查詢變成了等值查詢,數(shù)據(jù)可能不是太及時(shí)變化,就看業(yè)務(wù)是否允許了。
總之就是想辦法最大限度的利用索引。
到此這篇關(guān)于MySQL 索引優(yōu)化案例的文章就介紹到這了,更多相關(guān)MySQL 索引 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SQLyog連接MySQL8.0+報(bào)錯(cuò):錯(cuò)誤碼2058的解決方案
使用sqlyog連接數(shù)據(jù)庫(kù)過(guò)程中出現(xiàn)2058錯(cuò)誤,出現(xiàn)的原因是因?yàn)镸YSQL8.0對(duì)密碼的加密方式進(jìn)行了改變,這篇文章主要給大家介紹了關(guān)于SQLyog連接MySQL8.0+報(bào)錯(cuò):錯(cuò)誤碼2058的解決方案,需要的朋友可以參考下2024-07-07
MySQL 8.0.18 穩(wěn)定版發(fā)布! Hash Join如期而至
MySQL 8.0.18 穩(wěn)定版發(fā)布! Hash Join 如期而至,這篇文章帶大家快速瀏覽一下MySQL 8.0.18 穩(wěn)定版的各個(gè)亮點(diǎn),感興趣的小伙伴們可以學(xué)習(xí)參考一下2019-10-10
MySQL8 臨時(shí)關(guān)閉緩存的方法實(shí)現(xiàn)
在某些場(chǎng)景下,如基準(zhǔn)測(cè)試、數(shù)據(jù)一致性檢查或高頻數(shù)據(jù)更新,可能需要臨時(shí)關(guān)閉緩存以獲得更準(zhǔn)確的性能數(shù)據(jù)或解決性能問(wèn)題,本文就詳細(xì)的介紹一下MySQL8 臨時(shí)關(guān)閉緩存的方法實(shí)現(xiàn),感興趣的可以了解一下2024-10-10
最新MySQL數(shù)據(jù)庫(kù)漏洞情況通報(bào)
本文是對(duì)近期mysql報(bào)出的漏洞情況進(jìn)行了簡(jiǎn)單的說(shuō)明以及漏洞的修復(fù)措施分享,有需要的小伙伴一定要關(guān)注下2016-09-09
mysql二進(jìn)制日志文件恢復(fù)數(shù)據(jù)庫(kù)
喜歡的在服務(wù)器或者數(shù)據(jù)庫(kù)上直接操作的兄弟們你值得收藏下!不然你就悲劇了。-----(當(dāng)然我也是在網(wǎng)上搜索的資料!不過(guò)自己測(cè)試通過(guò)了的!)2014-08-08

