1. 程式人生 > 其它 >Mysql索引優化實戰一

Mysql索引優化實戰一

Mysql索引優化實戰一

Mysql索引底層資料結構 https://www.cnblogs.com/coderD/p/14957167.html

Explain詳解與索引最佳實踐 https://www.cnblogs.com/coderD/p/14961811.html

MySQL的內部元件結構 https://www.cnblogs.com/coderD/p/14961939.html

Mysql索引優化實戰一 https://www.cnblogs.com/coderD/p/14962474.html

示例表

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 '入職時間',
 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());

 ‐‐ 插入一些示例資料
 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();

1、聯合索引第一個欄位用範圍不會走索引

EXPLAIN SELECT * FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

結論:聯合索引的第一個欄位就用範圍查詢所以不會走索引,mysql內部可能會覺得第一個欄位就用了範圍,結果集應該很大,而且這樣的話,回表效率不高,還不如去全表掃描。

2、強制走索引

EXPLAIN SELECT * FROM employees force index(idx_name_age_position) WHERE name > 'LiLei' AND age = 22 AND position ='manager';

結論:雖然使用了強制走索引讓聯合索引第一個欄位範圍查詢也走索引,掃描的行rows看上去也少了點,但是最終查詢效率不一定比全表 掃描高,因為回表效率不高

3、覆蓋索引優化

EXPLAIN SELECT name,age,position FROM employees WHERE name > 'LiLei' AND age = 22 AND position ='manager';

ken_len列的74是這樣來的,當我們用utf-8的編碼方式,計算方法就為3 * n + 2,而我們當時建表的時候,name欄位是24位,也就是3 * 24 + 2 = 74,所以索引是name

4、in和or在表資料量比較大的情況會走索引,在表記錄不多的情況下會選擇全表掃描

EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','HanMeimei','Lucy') AND age = 22 AND position ='manager';
EXPLAIN SELECT * FROM employees WHERE (name = 'LiLei' or name = 'HanMeimei') AND age = 22 AND position
='manager';

5、like KK% 一般情況都會走索引

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

這裡給大家補充一個概念,索引下推(Index Condition Pushdown,ICP), like KK%其實就是用到了索引下推優化

什麼是索引下推了?

對於輔助的聯合索引(name,age,position),正常情況按照最左字首原則,SELECT * FROM employees WHERE name like 'LiLei%' AND age = 22 AND position ='manager' 這種情況只會走name欄位索引,因為根據name欄位過濾完,得到的索引行裡的age和 position是無序的,無法很好的利用索引。

在MySQL5.6之前的版本,這個查詢只能在聯合索引裡匹配到名字是 'LiLei' 開頭的索引,然後拿這些索引對應的主鍵逐個回表,到主鍵索 引上找出相應的記錄,再比對age和position這兩個欄位的值是否符合。

MySQL 5.6引入了索引下推優化,可以在索引遍歷過程中,對索引中包含的所有欄位先做判斷,過濾掉不符合條件的記錄之後再回表,可 以有效的減少回表次數。使用了索引下推優化後,上面那個查詢在聯合索引裡匹配到名字是 'LiLei' 開頭的索引之後,同時還會在索引裡過 濾age和position這兩個欄位,拿著過濾完剩下的索引對應的主鍵id再回表查整行資料。 索引下推會減少回表次數,對於innodb引擎的表索引下推只能用於二級索引,innodb的主鍵索引(聚簇索引)樹葉子節點上儲存的是全 行資料,所以這個時候索引下推並不會起到減少查詢全行資料的效果。

總結:索引下推就是當我們執行一條sql語句的時候,我們會向後看包含索引在內的所有字元,然後過濾掉不符合的記錄再回表

為什麼範圍查詢Mysql沒有用索引下推優化?

​ 估計應該是Mysql認為範圍查詢過濾的結果集過大,like KK% 在絕大多數情況來看,過濾後的結果集比較小,所以這裡Mysql選擇給 like KK% 用了索引下推優化,當然這也不是絕對的,有時like KK% 也不一定就會走索引下推。

Mysql如何選擇合適的索引

 EXPLAIN select * from employees where name > 'a'

如果用name索引需要遍歷name欄位聯合索引樹,然後還需要根據遍歷出來的主鍵值去主鍵索引樹裡再去查出最終資料,成本比全表掃描 還高,可以用覆蓋索引優化,這樣只需要遍歷name欄位的聯合索引樹就能拿到所有結果,如下:

EXPLAIN select name,age,position from employees where name > 'a' ;
 EXPLAIN select * from employees where name > 'zzz' ;

​ 對於上面這兩種 name>'a' 和 name>'zzz' 的執行結果,mysql最終是否選擇走索引或者一張表涉及多個索引,mysql最 終如何選擇索引,我們可以用trace工具來一查究竟,開啟trace工具會影響mysql效能,所以只能臨時分析sql使用,用 完之後立即關閉

trace工具用法:

set session optimizer_trace="enabled=on",end_markers_in_json=on; ‐‐開啟trace
select * from employees where name > 'a' order by position;
SELECT * FROM information_schema.OPTIMIZER_TRACE;

我將部分主要的細節貼在下面:

結論:全表掃描的成本小於使用索引查詢的成本,所以最後選擇了全表掃描。

mysql> select * from employees where name > 'zzz' order by position;
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;

檢視trace欄位可知索引掃描的成本低於全表掃描,所以mysql最終選擇索引掃描
set session optimizer_trace="enabled=off"; ‐‐關閉trace

常見sql深入優化

Order by與Group by優化

Case1:

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' and position = 'dev' ORDER BY age;

分析:

​ 利用最左字首法則:中間欄位不能斷,因此查詢用到了name索引,從key_len=74也能看出,age索引列用 在排序過程中,因為Extra欄位裡沒有using filesort

Case 2:

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' ORDER BY position;

分析:

從explain的執行結果來看:key_len=74,查詢使用了name索引,由於用了position進行排序,跳過了 age,出現了Using filesort。

Case 3:

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' ORDER BY age,position;

分析:

​ 查詢只用到索引name,age和position用於排序,無Using filesort。

Case 4:

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' ORDER BY position,age;

分析:

和Case 3中explain的執行結果一樣,但是出現了Using filesort,因為索引的建立順序為 name,age,position,但是排序的時候age和position顛倒位置了。

Case 5:

EXPLAIN SELECT * FROM employees WHERE name = 'LiLei' and age = 18 ORDER BY position,age;

分析:

​ 與Case 4對比,在Extra中並未出現Using filesort,因為age為常量,在排序中被優化,所以索引未顛倒, 不會出現Using filesort。

Case 6:

EXPLAIN SELECT * FROM employees WHERE name = 'zhuge' ORDER BY age asc,position DESC;

分析:

​ 雖然排序的欄位列與索引順序一樣,且order by預設升序,這裡position desc變成了降序,導致與索引的 排序方式不同,從而產生Using filesort。Mysql8以上版本有降序索引可以支援該種查詢方式。

Case 7:

EXPLAIN SELECT * FROM employees WHERE name in ('LiLei','zhuge') ORDER BY age,position;

分析:

​ 對於排序來說,多個相等條件也是範圍查詢

Case 8:

EXPLAIN SELECT * FROM employees WHERE name > 'a' ORDER BY name;

可以用覆蓋索引優化

EXPLAIN SELECT name,age,position FROM employees WHERE name > 'a' ORDER BY name;

優化總結:

1、MySQL支援兩種方式的排序filesort和index,Using index是指MySQL掃描索引本身完成排序。index 效率高,filesort效率低。

2、order by滿足兩種情況會使用Using index。

​ 1) order by語句使用索引最左前列。

​ 2) 使用where子句與order by子句條件列組合滿足索引最左前列。

3、儘量在索引列上完成排序,遵循索引建立(索引建立的順序)時的最左字首法則。

4、如果order by的條件不在索引列上,就會產生Using filesort。

5、能用覆蓋索引儘量用覆蓋索引

6、group by與order by很類似,其實質是先排序後分組,遵照索引建立順序的最左字首法則。對於group by的優化如果不需要排序的可以加上order by null禁止排序。注意,where高於having,能寫在where中 的限定條件就不要去having限定了。

Using filesort檔案排序原理詳解

filesort檔案排序方式

  • 單路排序:是一次性取出滿足條件行的所有欄位,然後在sort buffer中進行排序;用trace工具可 以看到sort_mode資訊裡顯示< sort_key, additional_fields >或者< sort_key, packed_additional_fields >
  • 雙路排序(又叫回表排序模式):是首先根據相應的條件取出相應的排序欄位和可以直接定位行 資料的行 ID,然後在 sort buffer 中進行排序,排序完後需要再次取回其它需要的欄位;用trace工具 可以看到sort_mode資訊裡顯示< sort_key, rowid >

MySQL 通過比較系統變數 max_length_for_sort_data(預設1024位元組) 的大小和需要查詢的欄位總大小來 判斷使用哪種排序模式。

  • 如果 欄位的總長度小於max_length_for_sort_data ,那麼使用 單路排序模式;
  • 如果 欄位的總長度大於max_length_for_sort_data ,那麼使用 雙路排序模∙式。

下圖,我們不考慮mysql的底層資料結構和其他因素,進行剖析,目的是為了講清楚什麼是單路排序和多路排序

我們先看單路排序的詳細過程:

  1. 從索引name找到第一個滿足 name = ‘zhuge’ 條件的主鍵 id
  2. 根據主鍵 id 取出整行,取出所有欄位的值,存入 sort_buffer 中
  3. 從索引name找到下一個滿足 name = ‘zhuge’ 條件的主鍵 id
  4. 重複步驟 2、3 直到不滿足 name = ‘zhuge’
  5. 對 sort_buffer 中的資料按照欄位 position 進行排序
  6. 返回結果給客戶端

我們再看下雙路排序的詳細過程:

  1. 從索引 name 找到第一個滿足 name = ‘zhuge’ 的主鍵id
  2. 根據主鍵 id 取出整行,把排序欄位 position 和主鍵 id 這兩個欄位放到 sort buffer 中
  3. 從索引 name 取下一個滿足 name = ‘zhuge’ 記錄的主鍵 id
  4. 重複 3、4 直到不滿足 name = ‘zhuge’
  5. 對 sort_buffer 中的欄位 position 和主鍵 id 按照欄位 position 進行排序
  6. 遍歷排序好的 id 和欄位 position,按照 id 的值回到原表中取出 所有欄位的值返回給客戶端

其實對比兩個排序模式,單路排序會把所有需要查詢的欄位都放到 sort buffer 中,而雙路排序只會把主鍵 和需要排序的欄位放到 sort buffer 中進行排序,然後再通過主鍵回到原表查詢需要的欄位。

如果 MySQL 排序記憶體 sort_buffer 配置的比較小並且沒有條件繼續增加了,可以適當把 max_length_for_sort_data 配置小點,讓優化器選擇使用雙路排序演算法,可以在sort_buffer 中一次排序更 多的行,只是需要再根據主鍵回到原表取資料。

如果 MySQL 排序記憶體有條件可以配置比較大,可以適當增大 max_length_for_sort_data 的值,讓優化器 優先選擇全欄位排序(單路排序),把需要的欄位放到 sort_buffer 中,這樣排序後就會直接從記憶體裡返回查詢結果了。

所以,MySQL通過 max_length_for_sort_data 這個引數來控制排序,在不同場景使用不同的排序模式, 從而提升排序效率。

注意,如果全部使用sort_buffer記憶體排序一般情況下效率會高於磁碟檔案排序,但不能因為這個就隨便增 大sort_buffer(預設1M),mysql很多引數設定都是做過優化的,不要輕易調整。

索引設計原則

1、程式碼先行,索引後上

不知大家一般是怎麼給資料表建立索引的,是建完表馬上就建立索引嗎?

這其實是不對的,一般應該等到主體業務功能開發完畢,把涉及到該表相關sql都要拿出來分析之後再建立 索引。

2、聯合索引儘量覆蓋條件

比如可以設計一個或者兩三個聯合索引(儘量少建單值索引),讓每一個聯合索引都儘量去包含sql語句裡的 where、order by、group by的欄位,還要確保這些聯合索引的欄位順序儘量滿足sql查詢的最左字首原 則。

3、不要在小基數字段上建立索引

索引基數是指這個欄位在表裡總共有多少個不同的值,比如一張表總共100萬行記錄,其中有個性別欄位, 其值不是男就是女,那麼該欄位的基數就是2。

如果對這種小基數字段建立索引的話,還不如全表掃描了,因為你的索引樹裡就包含男和女兩種值,根本沒 法進行快速的二分查詢,那用索引就沒有太大的意義了。

一般建立索引,儘量使用那些基數比較大的欄位,就是值比較多的欄位,那麼才能發揮出B+樹快速二分查 找的優勢來。

4、長字串我們可以採用字首索引

儘量對欄位型別較小的列設計索引,比如說什麼tinyint之類的,因為欄位型別較小的話,佔用磁碟空間也會 比較小,此時你在搜尋的時候效能也會比較好一點。

當然,這個所謂的欄位型別小一點的列,也不是絕對的,很多時候你就是要針對varchar(255)這種欄位建立 索引,哪怕多佔用一些磁碟空間也是有必要的。

對於這種varchar(255)的大欄位可能會比較佔用磁碟空間,可以稍微優化下,比如針對這個欄位的前20個 字元建立索引,就是說,對這個欄位裡的每個值的前20個字元放在索引樹裡,類似於 KEY index(name(20),age,position)。

此時你在where條件裡搜尋的時候,如果是根據name欄位來搜尋,那麼此時就會先到索引樹里根據name 欄位的前20個字元去搜索,定位到之後前20個字元的字首匹配的部分資料之後,再回到聚簇索引提取出來 完整的name欄位值進行比對。

但是假如你要是order by name,那麼此時你的name因為在索引樹裡僅僅包含了前20個字元,所以這個排 序是沒法用上索引的, group by也是同理。所以這裡大家要對字首索引有一個瞭解。

5、where與order by衝突時優先where

在where和order by出現索引設計衝突時,到底是針對where去設計索引,還是針對order by設計索引?到 底是讓where去用上索引,還是讓order by用上索引?

一般這種時候往往都是讓where條件去使用索引來快速篩選出來一部分指定的資料,接著再進行排序。

因為大多數情況基於索引進行where篩選往往可以最快速度篩選出你要的少部分資料,然後做排序的成本可 能會小很多。

6、基於慢sql查詢做優化

可以根據監控後臺的一些慢sql,針對這些慢sql查詢做特定的索引優化。

關於慢sql查詢不清楚的可以參考這篇文章:https://blog.csdn.net/qq_40884473/article/details/89455740