「MySQL」高性能索引優化策略

MySQL知識梳理圖,一圖看完整篇文章: html

今天接上一篇『MySQL』揭開索引神祕面紗 討論了索引的實現原理,瞭解了大概的原理,接下來了解一下高性能索引的優化策略,這也是面試中常常會問到的問題。python

1. 工具 Explain

在詳細總結MySQL的索引優化策略以前,先給你們介紹一個工具,方便在查慢查詢的過程,排查大部分的問題:Explain。有關Explain的詳細介紹,能夠查看官網地址: dev.mysql.com/doc/refman/… 。這裏再給你們推薦一個學習方法,就是必定要去官網學習第一手資料,若是以爲英語閱讀有挑戰的朋友,建議仍是平時都積累看看英文文章,英語對於程序員來講很重要,先進的技術和理論不少資料都是英文版,而官網也是很是全的,要想成爲技術大牛,這是必須須要修煉的。 扯淡就到這裏,下面我簡單描述一下Explain怎麼使用。 舉例:mysql

mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
     +----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
     | id | select_type | table | partitions | type | possible_keys | key        | key_len | ref               | rows | filtered | Extra |
     +----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
     |  1 | SIMPLE      | user  | NULL       | ref  | unique_key    | unique_key | 249     | const,const,const |    1 |   100.00 | NULL  |
     +----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
複製代碼

Explain 結果有好幾列,簡單說一下經常使用的列:select_type, type, key, key_len, ref, rows。其他列能夠參考官網介紹。程序員

  • select_type,是說查詢的類型,是簡單的查詢仍是複雜的查詢,若是不是涉及子查詢和UNION,select_type就是SIMPLE。其他的複雜查詢還有SUBQUERY和UNION等。
  • type, 很是重要,常常查詢分析時用到,type有幾個值ALL、index、range、ref、const(system)、NULL。ALL表明全表掃描,從頭掃到尾;index跟全表掃描同樣,只不過MySQL掃描表的時候是按照索引次序進行而不是行;range,範圍掃描,即有限制的索引掃描,開始於索引的某一點,返回匹配這個值域的行。ref,索引訪問,返回全部匹配某個單個值得行。const,常量,查詢的某部分優化轉換成一個常量。NULL,通常就是說執行的時候用不着再訪問表或者索引。查詢速度類型排序:const > ref > range > index=ALL。
  • key,這個好理解,用到了哪一個索引。
  • key_len。索引裏使用的字節數。
  • ref,表示key列記錄的索引中查找所用的列或者常量。

2. 準備Table

CREATE TABLE `user` (
 `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
 `gender` varchar(16) DEFAULT NULL,
 `name` varchar(64) DEFAULT NULL,
 `birthday` varchar(16) NOT NULL,
 `age` int(11) unsigned NOT NULL,
 PRIMARY KEY (`id`),
 KEY `unique_key` (`name`,`age`,`birthday`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
複製代碼

往表裏插入了一些數據,方便下面問題的分析面試

3. B-Tree索引場景和相關限制:

B-Tree索引,按上一篇原理分析知道是按順序存儲數據的,因此並非只要查詢語句中用了索引就能起做用的,下面來看看具體的場景和限制sql

  1. 全值匹配。 全值匹配指的是和索引中的全部列進行匹配,例如:
mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key        | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | unique_key    | unique_key | 249     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------------+---------+-------------------+------+----------+-------+
複製代碼

全值匹配,即按照索引的全部列均精確匹配,從ref和key_len看出,從語句用到了三個索引。理論上索引對順序是比較敏感的,但實際上執行下面語句能夠看看結果:緩存

explain select * from user where age=9099 and birthday="1980-08-02" and name="xiao";
複製代碼

結果答案是同樣的,由於MySQL查詢優化器會自動調整where子句的條件順序,從而匹配最適合的索引。bash

  1. 匹配最左前綴 。若是想查找name=xiao的全部人,即只使用索引的第一列。
mysql> explain select * from user where name="xiao";
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key        | key_len | ref   | rows  | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | unique_key    | unique_key | 195     | const | 15170 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------------+---------+-------+-------+----------+-------+
複製代碼

能夠看到用到了name這個索引。若是沒有匹配最左前綴,結果是怎麼樣了:服務器

mysql> explain select * from user where birthday="1980-08-02";
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows  | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 30340 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
複製代碼

能夠看到若是沒有用name查詢索引,則變成了全表查詢。函數

  1. 匹配列前綴。也就是說能夠只匹配某一列的值的開頭部分,例如想匹配name=xiao-1開頭的數據
mysql> explain select * from user where name like "xiao-1%";
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | unique_key    | unique_key | 195     | NULL | 1111 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
複製代碼

能夠看到類型是range, 使用key=unique_key的聯合索引。 若是是name like "%xiao-1%" 則就不能使用索引了,其中緣由能夠根據B-Tree的特性想一下。

  1. 匹配範圍值 。例如,想查找查找name在[xiao-1, xiao-200]之間的數據。
mysql> explain select * from user where name >  "xiao-1" and name <= "xiao-200";
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | unique_key    | unique_key | 195     | NULL | 1113 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
複製代碼

能夠看出type=range,用到了unique_key索引。

  1. 精確匹配某一列並範圍匹配另一列。好比想查 name="xiao", age在[1,100]之間的數據。
mysql> explain select * from user where name="xiao" and age > 1 and age < 100;
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key        | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | unique_key    | unique_key | 199     | NULL |   98 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------------+---------+------+------+----------+-----------------------+
複製代碼

能夠從key_len的長度參考上一條範圍匹配,發現key_len的長度變長了,實際就是用到了name和age2個索引,name是精準匹配,age是範圍匹配。 思考: 若是sql語句變成:

select * from user where name="xiao" and age > 1 and age < 100 and birthday="2000-08-02";
複製代碼

birthday的索引會用到嗎?

剛纔上面也提到了B-Tree索引有一些限制,如今總結一下:

  • 最左前綴原理,若是不是按照索引的最左列開始查找,則沒法使用索引。
  • 不能跳過索引中的列,好比索引是(name, age, birthday),那麼若是隻提供name, birthday兩列,則birthday的索引是沒法使用的。
  • 若是查詢中的某個列範圍查詢,則其右邊全部列都沒法使用索引。從第5點最後的思考題就是說的這一點限制。
  • 若是查詢語句有函數或者表達式,都是無法使用索引的,好比age-1=18,或者left(name, 3) = xia 。
  • 匹配列前綴,也就是上面提到的第三點,若是like表達式是「%xiao-1%」,則也是無法使用索引的。

4. 索引策略

先總結一下索引的優勢:

  • 索引大大減小了服務器須要掃描的數據量
  • 索引能夠幫助服務器避免排序和臨時表
  • 索引能夠將隨機I/O變成順序I/O

說了三大優勢,是否是以爲只要是個表,是個列就所有加上索引就行了?

這樣顯示是不對的,雖然索引雖然加快了查詢速度,但索引也是有代價的:索引文件自己要消耗存儲空間,同時索引會加劇插入、刪除和修改記錄時的負擔,另外,MySQL在運行時也要消耗資源維護索引,所以索引並非越多越好。 只要當索引幫助存儲引擎快速查找到記錄帶來的好處大於其帶來的額外工做時,索引纔是比較有效的。

那是否有什麼辦法知道何時該用索引,何時不應用了?

  • 表記錄比較少,簡單的全表掃描更高效。少的界定的話通常也是靠經驗,沒有明確多少行算少,我的以爲2000行之內就ok的,實際業務中不少配置表,明顯以爲不會有2000行的均可以。

  • 索引選擇性。高性能MySQL(第三版)對索引選擇性的定義是:不重複的索引值(也稱爲基數,cardinality)和數據表的記錄總數(#T)的比值,範圍從1/#T到1之間。索引選擇性越高則查詢效率越高,由於選擇性高的索引可讓MySQL在查找時過濾掉更多的行。惟一索引的選擇性是1,下面舉例看一下如何計算選擇性:

    mysql> select count( distinct name) / count(1) from user;
    +----------------------------------+
    | count( distinct name) / count(1) |
    +----------------------------------+
    |                           0.6632 |
    +----------------------------------+
    
    mysql> select count( distinct birthday) / count(1) from user;
    +--------------------------------------+
    | count( distinct birthday) / count(1) |
    +--------------------------------------+
    |                               0.0002 |
    +--------------------------------------+
    
    mysql> select count( distinct id) / count(1) from user;
    +--------------------------------+
    | count( distinct id) / count(1) |
    +--------------------------------+
    |                         1.0000 |
    +--------------------------------+
    複製代碼

    上面能夠看出,user表裏,name索引的選擇性還蠻高,id自增主鍵選擇性就是1,birthday的選擇性很低,其實沒有必要作索引了。說白了就是不能有效區分數據的列不適合作索引列(如性別,男女未知,最多也就三種,區分度很是低)。

    接下來總結一下常見的索引策略:

    • 獨立的列: 獨立的列是指索引的列不能使表達式的一部分,也不能是函數的參數。這個上面也有提到到,就再也不重複。
    • 前綴索引: 有時候須要索引很長的字符列,好比name,名字這種字符通常比較長,若是做爲索引,會將整個索引文件變得很大,也會致使查詢速度慢下來。一種方法只索引開始的部分字符,這樣能夠大大節約索引空間,從而提升索引效率。固然,這種優化也會下降索引的選擇性,舉例以下:
    mysql> select count( distinct left(name, 8)) / count(1) from user;
    +-------------------------------------------+
    | count( distinct left(name, 8)) / count(1) |
    +-------------------------------------------+
    |                                    0.3648 |
    +-------------------------------------------+
    
    mysql> select count( distinct left(name, 9)) / count(1) from user;
    +-------------------------------------------+
    | count( distinct left(name, 9)) / count(1) |
    +-------------------------------------------+
    |                                    0.6630 |
    +-------------------------------------------+
    複製代碼

    能夠看到當採用name,前綴8個字符時,選擇性還比較低,當變成9個字符時,選擇性就高了不少,修改索引爲left(name, 9),看一下索引的長度下降了多少了。 將索引改成(name(9), age, birthday)

    mysql> explain select * from user where name="xiao" and age=9099 and birthday="1980-08-02";
    +----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
    | id | select_type | table | partitions | type | possible_keys | key         | key_len | ref               | rows | filtered | Extra       |
    +----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
    |  1 | SIMPLE      | user  | NULL       | ref  | unique_key2   | unique_key2 | 84      | const,const,const |    1 |   100.00 | Using where |
    +----+-------------+-------+------------+------+---------------+-------------+---------+-------------------+------+----------+-------------+
    複製代碼

    能夠對比全值匹配中的explain語句,key_len從249縮小到了84,縮小了三倍,大大減小了索引文件的大小,提升了效率。可是也有缺點,前綴索引對ORDER BY or GROUP BY操做無效。

  • 選擇合適的索引列順序,從場景分析中看,對於B-Tree索引是按順序存儲數據,因此選擇一個最合適的順序索引列對查詢很是有幫助,但這個也沒有比較直觀的方法,通常考慮選擇性和業務需求的特性。好比上面的例子,name的選擇性>age>birthday,且一般業務中按某個用戶的name查詢的場景會居多,因此索引的順序就是(name, age, birthday)。說白了就是較頻繁做爲查詢條件的字段纔去建立索引。

  • 聚簇索引的特性, 從上一篇索引的原理分析,InnoDB引擎使用的B-Tree索引就是聚簇索引,這類索引有什麼特性了,上一篇也提到過,InnoDB數據是按主鍵彙集,若是表沒有顯示定義主鍵,則InnoDB會優先選擇一個惟一的非空索引代替,若是找不到這樣的索引,會隱式定義一個主鍵來聚簇索引。 因此在選擇主鍵的時候,建議參考如下:

    • 佔的字符儘可能的小
    • 使用自增ID做爲主鍵
    • 更新頻繁的列最好不要做爲索引

    有些人以爲使用業務中的惟一字段做爲主鍵便可,不必選一個跟業務無關的自增id做爲主鍵,但我我的建議最好使用跟業務無關的自增ID做爲主鍵。緣由以下:

    • InnoDB數據按主鍵順序彙集存儲,數據記錄自己被存於主索引的葉子節點上。這就要求同一個葉子節點內(大小爲一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,所以每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,若是頁面達到裝載因子(InnoDB默認爲15/16),則開闢一個新的頁(節點)。 若是表使用自增主鍵,那麼每次插入新的記錄,記錄就會順序添加到當前索引節點的後續位置,當一頁寫滿,就會自動開闢一個新的頁。 若是用業務的惟一主鍵,可能非自增主鍵(如身份證號或學號等),因爲每次插入主鍵的值近似於隨機,所以每次新紀錄都要被插到現有索引頁得中間某個位置,此時MySQL不得不爲了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上讀回來,這增長了不少開銷,同時頻繁的移動、分頁操做形成了大量的碎片,獲得了不夠緊湊的索引結構,後續不得不經過OPTIMIZE TABLE來重建表並優化填充頁面。可見插入的消耗是巨大的。

    • 爲何主鍵的字符要小了,由於二級索引是根據主鍵來檢索數據,則葉子節點存儲了主鍵列,也就是說二級索引的訪問須要訪問二次主鍵索引,若是主鍵索引很大,二級索引的可能比想象的要大不少,從而影響性能。

    • 更新頻繁的列最好不要做爲索引,若是更新頻繁的列做爲索引,每次更新,爲了保持有順,須要調整整個索引B-Tree樹,這樣的消耗也是挺大的。

  • 冗餘和重複索引 MySQL容許相同列上建立多個索引,有時候看到建了一個UNIQUE KEY (name, age),而後還建了個 KEY (name),這樣name這個索引就重複了,發現則須要刪除單獨的索引,能夠減小不少開銷。索引越多,會致使插入數據變慢。

  • 未使用的索引 在設計表的時候,剛開始需求可能須要用到某個字段的去查詢,就將此字段增長了索引,可能最後需求變動的時候,這個字段基本不多有場景去查,這時候常常會忘記去刪除此索引,致使不必的開銷。因此不必的索引,最好是刪除。

5. 大表如何刪除無用數據:

若是一張表百萬級以上,索引是須要額外的維護成本,由於索引文件是單獨存在的文件,因此當咱們對數據的增刪改,都會產生額外的對索引文件的操做,這些操做須要消耗額外的IO,會下降增刪改的執行效率。且刪除數據的速度跟建立的索引的數量是成正比的。有一個小技巧,能夠參考:

  • 先刪除索引,若是直接刪除數據,會帶來索引樹的數據大規模的調整,消耗沒法預估。
  • 而後刪除無用數據,這時候沒有索引,刪除無用數據的速度將會快不少。
  • 刪除數據後再重建索引,這時候數據也少了一些,速度也會相對快一點。

上面三個步驟比直接刪除確定是要快一點,若是直接刪除數據的過程當中刪除失敗,致使事務回滾,那消耗就成倍增長了。

索引策略就說這麼多,下一篇總結MySQL增刪改查和多表查詢優化。

更多精彩文章,請關注公衆號: 「天澄技術雜談」

相關文章
相關標籤/搜索