MySQL創建高性能的索引

標籤(空格分隔): 高性能MYSQL 第五章 創建高性能的索引


  在MySQL中,索引是在存儲引擎層而不是服務器層實現的。不同存儲引擎的索引的工作方式並不一樣,也不是所有的存儲引擎都支持所有類型的索引。下面我們先來看看MySQL支持的索引類型,以及它們的優點和缺點。

  1. 索引的種類

    1.1. B-Tree索引

      當人們談論索引的時候,多半說的是B-Tree索引,它使用B-Tree數據結構來存儲數據。我們使用術語“B-Tree”,是因爲MySQL在CREATE TABLE和其他語句中 使用該關鍵字。實際上,底層在存儲引擎也可能使用不同的存儲結構,InnoDB使用的是B+Tree。
      
      InnoDB按照數據格式對索引進行存儲,再根據主鍵引用被索引的行。B-Tree通常意味着所有的值都是按順序存儲的,並且每一個葉子到根的距離相同。
      
      B-Tree索引能夠加快訪問數據的速度,因爲存儲引擎不再需要進行全表掃描來獲取需要的數據,取而代之的是從索引的根節點開始進行搜索。根節點的槽中存放了指向子節點的指針,存儲引擎根據這些指針向下查找。葉子節點比較特別,它們的指針指向的是被索引的數據,面不是其他的節點頁(不同引擎的“打針”類型不同)。樹的深度和表的大小直接相關。
      
      B-Tree對索引列是順序組織存儲的,所以很適合查找範圍數據。所以像“找出所有以I到K開頭的名字”這樣的查找效率會非常高。
      (B-Tree的具體實現請自行百度)

    B-Tree索引對如下類型的查詢有效:

    • 全值匹配
      全值匹配指的是和索引中的所有列進行匹配。
    • 匹配最左前綴
    • 匹配列前綴
    • 匹配範圍值
    • 精確匹配某一列並範圍匹配另外一列
    • 只訪問索引的查詢
    • 覆蓋索引,索引包含查詢所有的數據,無須訪問數據行

        因爲索引樹中的節點是有序的,所以除了按值查找之外,索引還可能用於查詢中的ORDER BY操作。
        
      B-Tree索引的限制:

    • 如果不是按照索引的最左列開始查找(跳過了某些列),則無法使用索引。

    • 不能跳過索引中的列。
    • 如果查詢中有某個列的範圍查詢,則其右邊所有的列都無法使用索引優化查找。

    1.2. 哈希索引

      哈希索引(hash index)基於哈希表實現,只有精確匹配索引所有列的查詢纔有效。在MySQL中,只有Memory引擎顯示支持哈希索引。值得 一提的是 ,Memory引擎是支持非唯一哈希索引的,如果多個列的哈希值相同,索引會以鏈表的方式存放多個記錄指針到同一個哈希目中。
      
    1.2.1 哈希索引的限制:

    • 哈希索引只包含哈希值和行指針,而 不存儲字段值,所以不能使用索引中的值來避免讀取行。
    • 哈希索引數據並不是按照索引順序存儲的,所以也就無法用於排序。
    • 哈希索引也不支持部分索引列匹配查找,因爲哈希索引始終是使用索引列的全部內容來計算哈希值。
    • 哈希索引只支持等值比較查詢,包括=、IN()、<=>(注意<>和<=>是不同的操作,<=>可用於比較NULL)。也不支持任何範圍查詢。
    • 訪問哈希索引的數據非常快,除非有很多哈希衝突。出現衝突時需要遍歷列表。
    • 如果哈希衝突很多的話,一些索引維護操作的代價也會很高。刪除一行時,需要遍歷哈希值鏈表中的每一行。

    1.2.2 創建自定義哈希索引

      思路很簡單:在B-Tree基礎上創建一個僞哈希索引。這和真正的 哈希索引不是一回事,因爲還是使用B-Tree進行查找,但是它使用哈希值而不是鍵本身進行索引查找。你需要做的就是在查詢的WHERE子句中手動指定使用哈希函數。
    例:

    mysql> SELECT ID FROM url WHERE url="http://www.mysql.com"
              AND url_crc=CRC32("http://www.mysql.com");

      這樣做的性能會非常高,但缺陷是需要維護哈希值。可以手動維護,也可以 使用觸發器實現
      。如果採用這種方式,記住不要使用SHA1()和MD5()作爲哈希函數。因爲這兩個函數計算出來 的哈希值是非常長的字符串,會浪費大量空間,比較時也會更慢。
      CRC32)返回的是32位整數,當索引有93000條記錄時出現衝突的概率是1%。減少衝突可以使用FNV64()函數,或自定義哈希函數。
      

  2. 索引的優點

      索引可以讓服務器快速定位到表的指定位置。最常見的B-Tree索引,按照順序存儲數據,所以MySQL可以用來做ORDER BY和GROUP BY操作。
      總結下來索引有如下三個優點:
     1. 索引大大減少了服務器需要掃描的數據量。
     2. 索引可以幫助服務避免排序和臨時表。
     3. 索引可以將隨機I/O變爲順序I/O。

      索引評價的“三星系統”:索引將相關的記錄放到一起則獲得一星;如果索引中的數據順序和查找中的排列順序一致則獲得二星;如果索引中的列包含了查詢中需要的全部列則獲得“三星”。
      

  3. 高性能的索引策略

     3.1. 獨立的列
       索引不能是表達式的一部分,也不能是函數的參數。

    // 該查詢都不能使用索引
    mysql> SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
    mysql> SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;

    3.2. 前綴索引和索引選擇性

      索引的選擇性是指,不重複的索引值(也稱爲基數,cardinality)和數據表的記錄總數(#T)的比值,範圍從1/#T到1之間。索引的選擇性超高則查詢效率超高,因爲選擇性高的索引可以讓MySQL在查找時過濾掉更多的行。唯一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。
    選擇足夠長的前綴以保證較高的選擇性,同時又不能太長(以便節約空間)。前綴應該足夠長,以使得前綴索引的選擇性接近於索引整個列。換句話說,前綴的“基數”應該接近於完事列的“基數”。

      下面以實例講解:

    mysql> SELECT COUNT(*) AS cnt, city
    ->        FROM sakila.city_demo GROUP BY city ORDER BY cnt DESC LIMIT 10;
    
    +-----+----------------+
    | cnt | ciyt           |
    +-----+----------------+
    |  65 | London         |
    |  49 | hiroshima      |
    |  48 | Teboksary      |
    |  48 | Pak Kret       |
    |  48 | yaound         |
    |  47 | Tel Aviv-Jaffa |
    |  47 | Shimoga        |
    |  45 | Cabuyao        |
    |  45 | Callao         |
    |  45 | Bislig         |

      注意到,上面每個值都出現了45~65次。現在查找到最頻繁出現的城市前綴,從3個前綴字母到7個字母,經過實驗後發現前綴長度爲7時比較合適:
      

    mysql> SELECT COUNT(*) AS cnt, LEFT(city, 7) AS pref
    ->        FROM sakila.city_demo GROUP BY pref ORDER BY cnt DESC LIMIT 10;
    
    +-----+----------------+
    | cnt | ciyt           |
    +-----+----------------+
    |  70 | Santiag        |
    |  68 | San Fel        |
    |  65 | London         |
    |  61 | Valle d        |
    |  49 | Hiroshi        |
    |  48 | Teboksa        |
    |  48 | Pak Kre        |
    |  48 | Yaound         |
    |  47 | Tel Avi        |
    |  47 | Shimoga        |

      計算合適的前綴長度的另外一個辦法就是計算完整列的選擇性,併合前綴的選擇性接近於完整列的選擇性。下面顯示如何計算完整列的選擇性:

    msyql> SELECT COUNT(DISTINCT city)/COUNT(*) FROM sakila.city_demo;
    
    +-------------------------------+
    | COUNT(DISTINCT ciyt)/COUNT(*) |
    +-------------------------------+
    |                        0.0312 |
    +-------------------------------+

      下面給出瞭如何在同一個查詢中計算不同前綴長度的選擇性:

    mysql> SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) AS sel3,
    COUNT(DISTINCT LEFT(city, 4))/COUNT(*) AS sel4,
    COUNT(DISTINCT LEFT(city, 5))/COUNT(*) AS sel5,
    COUNT(DISTINCT LEFT(city, 6))/COUNT(*) AS sel6,
    COUNT(DISTINCT LEFT(city, 7))/COUNT(*) AS sel7
    FROM sakila.city_demo;
    
    +--------+--------+--------+--------+--------+
    | sel3   |  sel4  |  sel5  |  sel6  |  sel7  |
    +--------+--------+--------+--------+--------+
    | 0.0239 | 0.0293 | 0.0305 | 0.0309 | 0.0310 |
    +--------+--------+--------+--------+--------+

      查詢顯示當前綴長度到達7的時候,再增加前綴長度,選擇性提升的幅度已經很小了。
    注意:要同時考量平均值和選擇性,也就是上面的兩種評價。

    創建前綴索引:

    mysql> ALTER TABLE sakila.city_demo ADD KEY(city(7));

      MySQL無法使用前綴索引做ORDER BY和GROUP BY,也無法使用前綴索引做覆蓋掃描。

    3.3. 多列索引

      在MySQL5.0和更新的版本中,查詢能夠同時使用多個單列索引進行掃描,並將結果進行合併。這種算法有三個變種:OR條件的聚合(union),AND條件的相交(intersection),縱使前兩種情況的聯合及相交。

    mysql> EXPLAIN SELECT flim_id, actor_id FROM sakila.film_actor
        -> WHERE actor_id = 1 OR film_id = 1\G
    ************************1. row************************
    ...
    possible_key: PRIMARY,idx_fk_film_id
             key:PRIMARY,idx_fk_film_id
    ...
           Extra:Using  union(PRIMARY,idx_fk_film_id); Using Where

      索引合併策略有時候是一種優化的結果,但實際上更多時候說明了表上的索引建得很糟糕。
      另一種方法是改爲UNION查詢。

      3.4. 選擇合適的索引列順序
      在B-TREE索引中,將選擇性最高的列放到索引最前列,在某些場景可能有幫助,但通常不如避免隨機IO和排序那麼重要。
      性能不只是依賴於所有索引列的選擇性(整體基數),也和查詢條件的具體值有關,也就是和值的分佈 有關。

      3.5. 聚簇索引
      
      聚簇索引並不是一種單獨的索引類型,而是一種數據存儲方式。具體的細節依賴於其實現方式,但InnoDB的聚簇索引實際上在同一個 結構中保存了B-Tree索引和數據行。
      
      3.6. 覆蓋索引
      
      如果一個索引包含(或者說覆蓋)所有需要查詢的字段的值,我們就稱之爲“覆蓋索引”。覆蓋不需要滿足最左前綴的要求。
      這裏延伸出一種查詢優化方式,叫做“延遲關聯”
      

    mysql> SELECT * FROM products WHERE actor='SEAN CARREY'
        -> AND title like '%APOLLO%%';

    假設有一個 索引覆蓋一個數據列(actor,title,prod_id),重寫後的查詢:

    mysql> SELECT *
        -> FROM products
        ->    JOIN (
        ->       SELECT prod_id
        ->       FROM products
        ->       WHERE actor='SEAN CARREY' AND title LIKE '%APOLLO%'
        ->    ) AS t1 ON (t1.prod_id=products.prod_id);

      在查詢的第一階段MySQL可以使用覆蓋索引,在FROM子句的子查詢中找到匹配的prod_id,然後根據這些prod_id值在外層查詢匹配獲取需要的所有列值。

      3.7. 使用索引掃描來做排序
      
      MySQL有兩種方式可以生成有序的結果:通過排序操作;或者按索引順序掃描;如果EXPLAIN出來的type列的值爲index,則說明MySQL使用了索引掃描來做排序。
      
      掃描索引本身是很快的,因爲只需要從一條索引記錄移動到緊接着的下一條記錄。但如果索引不能覆蓋查詢所需要的全部列,那就不得不每掃描一條索引記錄就都回表查詢一次對應的行。這基本上都是隨機I/O,因此按索引順序讀取數據(全行記錄的數據)的速度通常要比順序地全表掃描慢,尤其是在I/O密集型的工作負載時。
      
      只有當索引的列順序和ORDER BY子句的順序完全一致,並且所有列的排序方向(倒序或正序)都一樣時,MySQL才能夠使用索引來對結果做排序。如果查詢需要關聯多張表,則只有當ORDER BY子句引用的字段全部爲第一個表時,才能使用索引做排序。ORDER BY子句和查找型查詢的限制是一樣的:需要滿足索引的最左前綴的要求;否則MySQL都需要執行排序操作,而無法利用索引排序。
      
      註解:type不爲index時,也可能是使用索引排序的,當EXTRA出現“Using filesort”時就需要優化。

      3.8. 索引和鎖
      
      索引可以讓查詢鎖定更少的行。如果你 的查詢從不訪問那些不需要的行,那麼就會鎖定更少的行。
      
      InnoDB只有在訪問行的時候纔會對其加鎖,而索引能夠減少InnoDB訪問的行數,從而減少鎖的數量。但這隻胡當InnoDB在存儲引擎層能夠過濾掉所有不需要的行時纔有效。如果索引無法過濾掉無效的行,那麼在InnoDB檢索到數據並返回給服務器層以後,MySQL服務器才能應用WHERE子句 。
      
      這時已經無法避免鎖定行了,InnoDB可以在服務器端過濾掉行後就釋放鎖,但是在早期MySQL版本中,InnoDB只胡在事務提交後才能釋放鎖。
      
      關於InnoDB、索引和鎖有一些很少有人知道的細節:InnoDB在二級索引上使用共享(讀)鎖,但訪問主鍵索引需要排他(寫)鎖。這消除了消除了使用覆蓋索引的可能性,並且使得SELECT FOR UPDATE男LOCK IN SHARE MODE或非鎖定查詢要慢很多。
      
      行級鎖又分共享鎖和排他鎖。
      
      共享鎖:
      名詞解釋:共享鎖又叫做讀鎖,所有的事務只能對其進行讀操作不能寫操作,加上共享鎖後在事務結束之前其他事務只能再加共享鎖,除此之外其他任何類型的鎖都不能再加了。
      

    // 用法:
    mysql> SELECT `id` FROM  table WHERE id in(1,2)   LOCK IN SHARE MODE
    // 結果集的數據都會加共享鎖

      排他鎖:
      名詞解釋:若某個事物對某一行加上了排他鎖,只能這個事務對其進行讀寫,在此事務結束之前,其他事務不能對其進行加任何鎖,其他進程可以讀取,不能進行寫操作,需等待其釋放。

    // 用法:
    mysql> SELECT `id` FROM mk_user WHERE id=1 FOR UPDATE
  4. 索引案例學習

      對於IN和範圍查詢,從EXPLAIN的輸出很難區分MySQL是要查詢範圍值,還是查詢列表值。EXPLAIN使用同樣的詞“range”來描述這兩種情況。

      從EXPLAIN的結果是無法區分這兩者的,但可以從值的範圍和多個等於條件來得出不同。在我們看來,IN查詢就是多個等值條件查詢。

      我們不是挑剔:這兩種訪問效率是不同的。對於範圍條件查詢,MySQL無法再使用範圍列後面的其他索引列了,但是對於“多人等值條件查詢”則沒有這個限制。

      排序優化
      使用延遲關聯
      注意:測試前請關係緩存

    mysql> SET SESSION query_cache_type=0;
    mysql> SELECT * FROM tt_test
         > ORDER BY score, value, id
         > LIMIT 100000, 100;

      重寫後的查詢:

    mysql> SELECT * FROM tt_test
         > INNER JOIN(
         >    SELECT id FROM tt_test
         >    ORDER BY score, VALUE, id
         >    LIMIT 100000, 100
         > )t USING(id);

      對越靠後的分頁越有效

  5. 維護索引和表

    對於InnoDB

    重建表(會重新組織數據)

    mysql> ALTER TABLE innodb_tb1 ENGINE=INNODB;

    更新索引統計信息

    mysql> ANALYZE TABLE
  6. 總結

      最後值得總的回顧一個這些特性以及如何使用B-Tree索引。
    在選擇索引和編寫利用這些索引的查詢時,有如下三個原則始終需要記住:

    1. 單行訪問是很慢的。特別是在機械硬盤存儲中(SSD的隨機I/O要快很多,不過這一點仍然成交)。如果服務器從存儲中讀取一個數據塊只是爲了獲取其中一行,那麼就浪費了很多工作。最好讀取的快中包含儘可能多所需要的行。使用索引可以創建位置引用以提升效率。

    2. 按順序訪問範圍數據是很快的,這有兩個原因。第一,順序I/O不需要多次磁盤尋道,所以比隨機I/O要快很多(特別是對機械硬盤)。第二,如果服務器能夠按需要順序讀取數據,那麼就不再需要額外的排序操作,並且GROUP BY查詢也無須再做排序和將按組進行聚合計算了。

    3. 索引覆蓋查詢是很快的。如果一個索引包含了查詢需要的所有列,那麼存儲引擎就不需要再回表查找行。這避免了大量的單選訪問,而上面的第上點已經寫明單選訪問是很慢的。

        總的來說,編寫查詢語句時應該盡選擇合適的索引以避免單選查找、儘可能地使用數據原生順序從而避免額外的排序操作,並儘可能使用索引覆蓋查詢。這與“三星”評價系統是一致的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章