一、背景
昨天早上,交流群有一位同學提出了一個問題。看下圖:
我不是大佬,而且當時我自己的想法也只是猜測,所以並沒有回復那位同學,只是接下來自己做了一個測試驗證一下。
他只簡單了說了一句話,就是同樣的sql,一個沒加 order by 就全表掃描,一個加了 order by 就走索引了。
我們可以仔細點看一下他提供的圖(主要分析子查詢即可,就是關於表 B 的查詢,因為只有表 B 的查詢前后不一致),我們可以先得出兩個前提:
1、首先可以肯定的是,where 條件中的 mobile 字段是沒有索引的。因為沒有 order by 時,是全表掃描,如果 mobile 字段有索引,查詢優化器必定會使用 mobile 字段的索引。
2、其實重點不但在 order by,更重要的是在於 order by 后面跟着的字段是 表B 的主鍵 id。之所以判斷 id 為主鍵,是因為 explain 執行計划里看到使用了 PRIMARY 索引,即主鍵索引。
二、數據准備和場景重現
創建表 user:
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100007 DEFAULT CHARSET=utf8;
准備數據:
看了一下截圖,數據量應該在10萬左右,我們也准備10萬數據,盡量做到一致。
delimiter ;
CREATE DEFINER=`root`@`localhost` PROCEDURE `iniData`()
begin
declare i int;
set i=1;
while(i<=100000)do
insert into user(name,age,phone) values('測試', i, 15627230000+i);
set i=i+1;
end while;
end;;
delimiter ;
call iniData();
執行 SQL ,查看執行計划:
explain select * from user where phone = '15627231000' limit 1;
explain select * from user where phone = '15627231000' order by id limit 1;
執行結果:
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE user (Null) ALL (Null) (Null) (Null) (Null) 99927 10 Using where
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE user (Null) index (Null) PRIMARY 4 1 10 Using where
我們可以看到,執行計划和那位同學的基本一致,都是第一條 SQL 全表掃描,第二條 SQL 是走了主鍵索引。
三、猜想和猜測着總結
只要加 order by 就走索引?
根據上面的執行計划來看,明顯這位同學的表達是不對的,更重要的是因為 order by 后跟着的字段是主鍵 id,所以才走了索引,走了主鍵索引。
我們可以試試用 age 字段來排序,這時候肯定是沒有走索引的,因為我們壓根沒有為 age 字段沒有建立索引。
explain select * from user where phone = '15627231000' order by age limit 1;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE user (Null) ALL (Null) (Null) (Null) (Null) 99927 10 Using where; Using filesort
分析:
首先,我們看到 type 是 ALL,就是全表掃描,而且我們還留意到:Extra的值多了 using filesort,表明 MySQL 有文件排序的操作。
我們可以拿 order by age 和 order by id 的執行計划來對比一下。
1、explain 的 tepe 字段:
首先,type 不一樣,一個是 index,表明利用了索引樹;一個是 ALL,表明是全表掃描。
2、explain 的 Extra 字段:
第二,也是最重點的,它其實可以說明為何利用了主鍵索引。就是 Extra 字段。
先說明一下正常的排序,Extra 都會有 Using filesort 來表明使用了文件排序。
而明顯 order by id 是沒有這個,這是因為,索引樹本來就是一個帶有順序的數據結構,大家不了解的可以去看看 B+Tree 的介紹。查詢優化器正是利用了索引的順序性,使得 SQL 的執行計划走主鍵索引樹來去掉原本需要的排序。
之前的大白話 MySQL 學習總結中也提到過查詢優化器
。SQL 的執行計划能有很多,並且結果是一樣的,但是為了提高性能,MySQL 的查詢優化器
組件會為 SQL 制定一套最優的執行計划。
階段總結:
查詢優化器幫我們制定的最優計划是:充分利用主鍵索引的順序性,避免了全表掃描后還是需要排序操作。
當然了,我們不能自己只是根據現象做判斷,下面將利用 Trace 來查看優化器追蹤的信息,進一步的驗證我們的總結是沒問題的。
四、通過 Trace 分析來驗證
開啟和查看 Trace
-- 開啟優化器跟蹤
set session optimizer_trace='enabled=on';
select * from user where phone = '15627231000' order by id limit 1;
-- 查看優化器追蹤
select * from information_schema.optimizer_trace;
下面我們只看 TRACE 就行了。
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `user`.`id` AS `id`,`user`.`name` AS `name`,`user`.`age` AS `age`,`user`.`phone` AS `phone` from `user` where (`user`.`phone` = '15627231000') order by `user`.`id` limit 1"
}
]
}
},
{
"join_optimization": { // 優化工作的主要階段
"select#": 1,
"steps": [
// .... 省略很多步驟
{
"reconsidering_access_paths_for_index_ordering": { // 重新考慮索引排序的訪問路徑
"clause": "ORDER BY",
"index_order_summary": {
"table": "`user`",
"index_provides_order": true,
"order_direction": "asc",
"index": "PRIMARY", // 排序的字段為主鍵 id,有主鍵索引
"plan_changed": true, // 改變執行計划
"access_type": "index"
}
}
},
{
"refine_plan": [
{
"table": "`user`"
}
]
}
]
}
},
{
"join_explain": {
"select#": 1,
"steps": [
]
}
}
]
}
好了,在最后的那里,我們看到了查詢優化器幫我們使用了主鍵索引。
所以,我們上面的猜想是正確的,因為 where 條件后的 phone 字段沒有加上索引,所以到 order by id 時,查詢優化器發現可以利用主鍵索引所以來避免排序,所以最后就使用了主鍵索引。
那么,按照上面的說法,如果 phone 字段加上了索引,那么最后應該就是走 phone 的索引而不是主鍵索引了。而且,SQL 調優有那么一條建議:建議經常在 where 條件后出現的字段加上索引來提高查詢性能。
下面我們來繼續驗證一下我們的猜想。
五、關於 where 條件字段索引和 order by 字段索引的選擇
1、給字段 phone 增加索引:
2、執行 SQL :
explain select * from user where phone = '15627231000' order by id limit 1;
3、結果:
我們可以看到,最后查詢優化器判斷 phone索引 比 主鍵索引 更能提高性能,所以使用了 phone 的索引。
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE user (Null) index index_phone index_phone 36 1 100 Using index condition
4、Trace進一步驗證:
最后,我們可以看到,查詢優化器否定了使用主鍵索引,不改變之前的執行計划。
-- 開啟優化器跟蹤
set session optimizer_trace='enabled=on';
select * from user where phone = '15627231000' order by id limit 1;
-- 查看優化器追蹤
select * from information_schema.optimizer_trace;
Trace 分析:
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `user`.`id` AS `id`,`user`.`name` AS `name`,`user`.`age` AS `age`,`user`.`phone` AS `phone` from `user` where (`user`.`phone` = '15627231000') order by `user`.`id` limit 1"
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
// .... 省略很多步驟
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`user`",
"best_access_path": {
"considered_access_paths": [
{
"access_type": "ref",
"index": "index_phone",
"rows": 1,
"cost": 1.2,
"chosen": true
},
{
"access_type": "range",
"range_details": {
"used_index": "index_phone" // 使用 phone 的索引
},
"chosen": false,
"cause": "heuristic_index_cheaper"
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 1,
"cost_for_plan": 1.2,
"chosen": true
}
]
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`user`.`phone` = '15627231000')",
"attached_conditions_computation": [
],
"attached_conditions_summary": [
{
"table": "`user`",
"attached": null
}
]
}
},
{
"clause_processing": {
"clause": "ORDER BY",
"original_clause": "`user`.`id`",
"items": [
{
"item": "`user`.`id`"
}
],
"resulting_clause_is_simple": true,
"resulting_clause": "`user`.`id`"
}
},
{
"added_back_ref_condition": "((`user`.`phone` <=> '15627231000'))"
},
{
"reconsidering_access_paths_for_index_ordering": { // 重新考慮索引排序的訪問路徑
"clause": "ORDER BY",
"index_order_summary": {
"table": "`user`",
"index_provides_order": true,
"order_direction": "asc",
"index": "index_phone",
"plan_changed": false // 不改變執行計划
}
}
},
{
"refine_plan": [
{
"table": "`user`",
"pushed_index_condition": "(`user`.`phone` <=> '15627231000')",
"table_condition_attached": null
}
]
}
]
}
},
{
"join_explain": {
"select#": 1,
"steps": [
]
}
}
]
}
六、最后總結
到這里,分析就結束了,我們可以得出一個結論,當然了,只是基於上面的實驗所得:
1、SQL 帶有 order by :
-
order by 后面的字段有索引:
-
where 條件后面的所有字段都沒索引,則使用 order by 后面的字段的索引。
-
where 條件后面有字段帶有索引,則使用 where 條件對應的字段的索引。
-
-
order by 后面的字段沒有索引:
- where 條件后面的所有字段都沒索引,則全表掃描。
- where 條件后面有字段帶有索引,則使用 where 條件后面的字段的索引。
2、SQL 不帶 order by:
-
where 條件后面的所有字段都沒索引,則全表掃描。
-
where 條件后面只要有字段帶索引,則使用該字段對應的索引。
最后我們也可以得出一個絕對的結論:查詢優化器是真的好使,哈哈哈!
七、題外話
其實上面的實驗需要大家對 MySQL 的索引原理有一定的了解,但是不用特別深。
如果大家感興趣的話,可以關注一下我現在寫的 【大白話系列】MySQL 學習總結 這一系列的文章,我會將自己學習 MySQL 后的學習總結分享在這里。