MySQL全面瓦解25:構建高效能索引(案例分析篇)

翁智華發表於2021-03-09

回顧一下上面幾篇索引相關的文章:

MySQL全面瓦解22:索引的介紹和原理分析

MySQL全面瓦解23:MySQL索引實現和使用

MySQL全面瓦解24:構建高效能索引(策略篇)

索引的十大原則

1、正確理解和計算索引欄位的區分度,文中有計算規則,區分度高的索引,可以快速得定位資料,區分度太低,無法有效的利用索引,可能需要掃描大量資料頁,和不使用索引沒什麼差別。

2、正確理解和計算字首索引的欄位長度,文中有判斷規則,合適的長度要保證高的區分度和最恰當的索引儲存容量,只有達到最佳狀態,才是保證高效率的索引。

3、聯合索引注意最左匹配原則:必須按照從左到右的順序匹配,MySQL會一直向右匹配索引直到遇到範圍查詢(>、<、between、like)然後停止匹配。

如   depno=1 and empname>'' and job=1 ,如果建立(depno,empname,job)順序的索引,job是用不到索引的。

4、應需而取策略,查詢記錄的時候,不要一上來就使用*,只取需要的資料,可能的話儘量只利用索引覆蓋,可以減少回表操作,提升效率。 

5、正確判斷是否使用聯合索引(上面聯合索引的使用那一小節有說明判斷規則),也可以進一步分析到索引下推(IPC),減少回表操作,提升效率。

6、避免索引失效的原則:禁止對索引欄位使用函式、運算子操作,會使索引失效。這是實際上就是需要保證索引所對應欄位的”乾淨度“。

7、避免非必要的型別轉換,字串欄位使用數值進行比較的時候會導致索引無效。

8、模糊查詢'%value%'會使索引無效,變為全表掃描,因為無法判斷掃描的區間,但是'value%'是可以有效利用索引。

9、索引覆蓋排序欄位,這樣可以減少排序步驟,提升查詢效率 

10、儘量的擴充套件索引,非必要不新建索引。比如表中已經有a的索引,現在要加(a,b)的索引,那麼只需要修改原來的索引即可。

查詢優化分析器 - explain

explain命令大家應該很熟悉,具體用法和欄位含義可以參考官網explain-output,這裡需要強調rows是核心指標,絕大部分rows小的語句執行一定很快,因為掃描的內容基數小。

所以優化語句基本上都是在優化降低rows值。

慢查詢優化基本步驟

1.先執行檢視實際耗時,判斷是否真的很慢(注意設定SQL_NO_CACHE)。

2.高區分度優先策略:where條件單表查,鎖定最小返回記錄表的條件。

就是查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每個欄位分別查詢,看哪個欄位的區分度最高。區分度高的欄位往前排。

3.explain檢視執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)

4.order by limit 形式的sql語句讓排序的表優先查

5.瞭解業務方的使用場景,根據使用場景適時調整。

6.加索引時參照建上面索引的十大原則

7.觀察結果,不符合預期繼續從第一步開始分析

查詢案例分析

下面幾個例子詳細解釋瞭如何分析和優化慢查詢。

複雜查詢條件的分析

一般來說我們編寫SQL的方式是為了 是實現功能,在實現功能的基礎上保證MySQL的執行效率也是非常重要的,這要求我們對MySQL的執行計劃和索引規則有非常清晰的理解,分析下面的案例:

1 mysql> select a.*,b.depname,b.memo from emp a left join 
2 dep b on a.depno = b.depno where sal>100 and a.empname like 'ab%'  and a.depno=106 order by a.hiredate desc ;
3 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+----------+
4 | id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno | depname    | memo     |
5 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+----------+
6 | 4976754 | 4976754 | ABijwE  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   106 | kDpNWugzcQ | TYlrVEkm |
7 ......
8 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+----------+
9 744 rows in set  (4.958 sec) 

總共就查詢了744條資料,卻耗費了4.958的時間,我們看一下目前表中現存的索引以及索引使用的情況分析

 1 mysql> show index from emp;
 2 +-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
 3 | Table | Non_unique | Key_name      | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
 4 +-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
 5 | emp   |          0 | PRIMARY       |            1 | id          | A         |     4952492 | NULL     | NULL   |      | BTREE      |         |               |
 6 | emp   |          1 | idx_emo_depno |            1 | depno       | A         |          18 | NULL     | NULL   |      | BTREE      |         |               |
 7 +-------+------------+---------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
 8 2 rows in set
 9 
10 mysql> explain select a.*,b.depname,b.memo from emp a left join 
11 dep b on a.depno = b.depno where sal>100 and a.empname like 'ab%'  and a.depno=106 order by a.hiredate desc ;
12 +----+-------------+-------+------+---------------+---------------+---------+-------+--------+-----------------------------+
13 | id | select_type | table | type | possible_keys | key           | key_len | ref   | rows   | Extra                       |
14 +----+-------------+-------+------+---------------+---------------+---------+-------+--------+-----------------------------+
15 |  1 | SIMPLE      | a     | ref  | idx_emo_depno | idx_emo_depno | 3       | const | 974898 | Using where; Using filesort |
16 |  1 | SIMPLE      | b     | ref  | idx_dep_depno | idx_dep_depno | 3       | const |      1 | NULL                        |
17 +----+-------------+-------+------+---------------+---------------+---------+-------+--------+-----------------------------+
18 2 rows in set 

可以看出,目前在emp表上除了主鍵只存在一個索引  idx_emo_depno ,作用在部門編號欄位上,該索引的目標是過濾出具體部門編號下的資料。

通過explain 分析器可以看到 where條件後面是走了   idx_emo_depno  索引,但是也比較了 97W的資料,說明該欄位的區分度並不高,根據高區分度優先原則,我們對這個表的三個查詢欄位分別進行區分度計算。

1 mysql> select count(distinct empname)/count(*),count(distinct depno)/count(*),count(distinct sal)/count(*) from emp; 
2 +----------------------------------+--------------------------------+------------------------------+
3 | count(distinct empname)/count(*) | count(distinct depno)/count(*) | count(distinct sal)/count(*) |
4 +----------------------------------+--------------------------------+------------------------------+
5 | 0.1713                           | 0.0000                         | 0.0000                       |
6 +----------------------------------+--------------------------------+------------------------------+
7 1 row in set 

這是計算結果,empname的區分度最高,所以合理上是可以建立一個包含這三個欄位的聯合索引,順序如下:empname、depno、sal;

並且查詢條件重新調整了順序,符合最左匹配原則;另一方面根據應需而取的策略,把b.memo欄位去掉了。

1 mysql> select a.*,b.depname from emp a left join 
2 dep b on a.depno = b.depno where  a.empname like 'ab%'  and a.depno=106 and a.sal>100 order by a.hiredate desc ;
3 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+
4 | id      | empno   | empname | job     | mgr | hiredate            | sal  | comn | depno | depname    |
5 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+
6 | 4976754 | 4976754 | ABijwE  | SALEMAN |   1 | 2021-01-23 16:46:24 | 2000 | 400  |   106 | kDpNWugzcQ |
7 ......
8 +---------+---------+---------+---------+-----+---------------------+------+------+-------+------------+
9 744 rows in set  (0.006 sec) 

這邊還有一個問題,那就是聯合索引根據最左匹配原則:必須按照從左到右的順序匹配,MySQL會一直向右匹配索引直到遇到範圍查詢(>、<、between、like)然後停止匹配。

所以語句中 執行到a.empname 欄位,因為使用了like,後面就不再走索引了。在這個場景中, 獨立的empname欄位上的索引和這個聯合索引效率是差不多的。

另外排序欄位hiredate也可以考慮到覆蓋到索引中,會相應的提高效率。

無效索引的分析

有一個需求,使用到了使用者表  userinfo 和消費明細表 salinvest ,目的想把2020年每個使用者在四個品類等級(A1、A2、A3、A4)上的消費額度進行統計,所以便下了如下的指令碼:

 1 select (@rowNO := @rowNo+1) AS id,bdata.* from 
 2 (
 3 select distinct a.usercode,a.username,
 4 @A1:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A1' 
 5 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A1,
 6 @A2:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A2' 
 7 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A2,
 8 @A3:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A3' 
 9 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A3,
10 @A4:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A4' 
11 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A4,
12 ,(@A1+@A2+@A3+@A4) as allnum 
13 from userinfo a 
14 inner JOIN `salinvest` b on a.usercode = b.usercode  
15 where b.logdate between '2020-01-01' and '2020-12-31'
16 order by allnum desc
17 ) as bdata,(SELECT @rowNO:=0) b; 

這個查詢看起來貌似沒什麼問題 ,雖然用到了複合查詢、子查詢,但是如果索引做的正確,也不會有什麼問題。那我們來看看索引,有一個聯合索引,符合我們最左匹配原則和高區分度優先原則:

 1 mysql> show index from salinvest;
 2 +------------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
 3 | Table      | Non_unique | Key_name               | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
 4 +------------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
 5 | lnuminvest |          0 | PRIMARY                |            1 | autocode    | A         |           5 | NULL     | NULL   |      | BTREE      |         |               |
 6 | lnuminvest |          1 | idx_salinvest_complex |            1 | usercode      | A         |           2 | NULL     | NULL   | YES  | BTREE      |         |               |
 7 | lnuminvest |          1 | idx_salinvest_complex |            2 | gravalue    | A         |           2 | NULL     | NULL   | YES  | BTREE      |         |               |
 8 | lnuminvest |          1 | idx_salinvest_complex |            3 | logdate     | A         |           2 | NULL     | NULL   | YES  | BTREE      |         |               |
 9 +------------+------------+------------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
10 4 rows in set 

那我們來看看它的執行效率:

mysql> select (@rowNO := @rowNo+1) AS id,bdata.* from 
(
select (@rowNO := @rowNo+1) AS id,bdata.* from 
(
select distinct a.usercode,a.username,
@A1:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A1' 
and c.logdate between '2020-01-01' and '2020-12-31'),0) as A1,
@A2:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A2' 
and c.logdate between '2020-01-01' and '2020-12-31'),0) as A2,
@A3:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A3' 
and c.logdate between '2020-01-01' and '2020-12-31'),0) as A3,
@A4:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A4' 
and c.logdate between '2020-01-01' and '2020-12-31'),0) as A4,
,(@A1+@A2+@A3+@A4) as allnum 
from userinfo a 
inner JOIN `salinvest` b on a.usercode = b.usercode  
where b.logdate between '2020-01-01' and '2020-12-31'
order by allnum desc
) as bdata,(SELECT @rowNO:=0) b;
+----+------------+---------+------+------+------+------+------+--------+
| id | usercode     | username | A1     | A2   | A3   | A4   |allnum
+----+------------+---------+------+------+------+------+------+--------+
|  1 | 063105015    | brand    | 789.00 | 1074.50 | 998.00 | 850.00 |  
......
+----+------------+---------+------+------+------+------+------+--------+
6217 rows in set  (12.745 sec) 

我這邊省略了查詢結果,實際上結果輸出6000多條資料,在約50W的資料中進行統計與合併,輸出6000多條資料,花費了將近13秒,這明顯是不合理的。

我們來分析下是什麼原因:

 1 mysql> explain select (@rowNO := @rowNo+1) AS id,bdata.* from 
 2 (
 3 select distinct a.usercode,a.username,
 4 @A1:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A1' 
 5 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A1,
 6 @A2:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A2' 
 7 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A2,
 8 @A3:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A3' 
 9 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A3,
10 @A4:=IFNULL((select sum(c.ltimenum) from `salinvest` c where c.usercode=a.usercode AND c.gravalue='A4' 
11 and c.logdate between '2020-01-01' and '2020-12-31'),0) as A4,
12 ,(@A1+@A2+@A3+@A4) as allnum 
13 from userinfo a 
14 inner JOIN `salinvest` b on a.usercode = b.usercode  
15 where b.logdate between '2020-01-01' and '2020-12-31'
16 order by allnum desc
17 ) as bdata,(SELECT @rowNO:=0) b;
18 +----+--------------------+------------+------------+--------+------------------------+------------------------+---------+-----------------------+------+----------+-----------------------------------------------------------+
19 | id | select_type        | table      | partitions | type   | possible_keys          | key                    | key_len | ref                   | rows | filtered | Extra                                                     |
20 +----+--------------------+------------+------------+--------+------------------------+------------------------+---------+-----------------------+------+----------+-----------------------------------------------------------+
21 |  1 | PRIMARY            | <derived8> | NULL       | system | NULL                   | NULL                   | NULL    | NULL                  |    1 |      100 | NULL                                                      |
22 |  1 | PRIMARY            | <derived2> | NULL       | ALL    | NULL                   | NULL                   | NULL    | NULL                  |    2 |      100 | NULL                                                      |
23 |  8 | DERIVED            | NULL       | NULL       | NULL   | NULL                   | NULL                   | NULL    | NULL                  | NULL | NULL     | No tables used                                            |
24 |  2 | DERIVED            | b          | NULL       | index  | idx_salinvest_complex | idx_salinvest_complex | 170     | NULL                  |    5 |       20 | Using where; Using index; Using temporary; Using filesort |
25 |  7 | DEPENDENT SUBQUERY | c          | NULL       | ALL    | idx_salinvest_complex | NULL                   | NULL    | NULL                  |    5 |       20 | Using where                                               |
26 |  6 | DEPENDENT SUBQUERY | c          | NULL       | ALL    | idx_salinvest_complex | NULL                   | NULL    | NULL                  |    5 |       20 | Using where                                               |
27 |  5 | DEPENDENT SUBQUERY | c          | NULL       | ALL    | idx_salinvest_complex | NULL                   | NULL    | NULL                  |    5 |       20 | Using where                                               |
28 |  4 | DEPENDENT SUBQUERY | c          | NULL       | ALL    | idx_salinvest_complex | NULL                   | NULL    | NULL                  |    5 |       20 | Using where                                               |
29 +----+--------------------+------------+------------+--------+------------------------+------------------------+---------+-----------------------+------+----------+-----------------------------------------------------------+
30 9 rows in set 

看最後四條資料,看他的possible_key和 實際的key,預估是走  idx_salinvest_complex  索引,實際是走了空索引,這個是為什麼呢? 看前面的select_type 欄位,值是 DEPENDENT SUBQUERY,瞭然了。

官方對 DEPENDENT SUBQUERY 的說明:子查詢中的第一個SELECT, 取決於外面的查詢 。

什麼意思呢?它意味著兩步:

第一步,MySQL 根據  select distinct a.usercode,a.username  得到一個大結果集 t1,這就是我們上圖提示的6000使用者。

第二步,上面的大結果集 t1 中的每一條記錄,等同於與子查詢 SQL 組成新的查詢語句: select sum(c.ltimenum) from `salinvest` c where c.usercode in (select distinct a.usercode from userinfo a) 。

也就是說, 每個子查詢要比較6000次,幾十萬的資料啊……即使這兩步驟查詢都用到了索引,但還是會很慢。

這種情況下, 子查詢的執行效率受制於外層查詢的記錄數,還不如拆成兩個獨立查詢順序執行呢

這種慢查詢的解決辦法,網上有很多方案,最常用的辦法是用聯合查詢代替子查詢,可以自己去查一下。

總結

上面給出了兩種典型的問題,一種是沒有使用索引使用原則進行索引構建,一種是遇到坑導致索引無效。我們在實際的應用中遇到過很多問題。比如:

1、不多的資料結果集,但是涉及到超多個表join的低效指令碼。

2、無效的聯表查詢,就是其中一張聯表沒有任何使用,但是資料基數極大。(這也可能是某個時候業務變更導致的sql指令碼忘了調整)。

3、varchar型別欄位等值比較沒有寫單引號,巨量基數笛卡爾積查詢直接把從庫搞死,在索引欄位上做計算導致索引失效的。

所有的這些案例都只是一些經驗積累,只有熟悉查詢優化器、索引的內部原理,瞭解索引優化的策略,才能定位這些問題的原因並加以解決。

相關文章