標籤(空格分隔): 高性能MYSQL 第五章 創建高性能的索引
在MySQL中,索引是在存儲引擎層而不是服務器層實現的。不同存儲引擎的索引的工作方式並不一樣,也不是所有的存儲引擎都支持所有類型的索引。下面我們先來看看MySQL支持的索引類型,以及它們的優點和缺點。
索引的種類
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()函數,或自定義哈希函數。
- 全值匹配
索引的優點
索引可以讓服務器快速定位到表的指定位置。最常見的B-Tree索引,按照順序存儲數據,所以MySQL可以用來做ORDER BY和GROUP BY操作。
總結下來索引有如下三個優點:
1. 索引大大減少了服務器需要掃描的數據量。
2. 索引可以幫助服務避免排序和臨時表。
3. 索引可以將隨機I/O變爲順序I/O。索引評價的“三星系統”:索引將相關的記錄放到一起則獲得一星;如果索引中的數據順序和查找中的排列順序一致則獲得二星;如果索引中的列包含了查詢中需要的全部列則獲得“三星”。
高性能的索引策略
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
索引案例學習
對於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);
對越靠後的分頁越有效
維護索引和表
對於InnoDB
重建表(會重新組織數據)
mysql> ALTER TABLE innodb_tb1 ENGINE=INNODB;
更新索引統計信息
mysql> ANALYZE TABLE
總結
最後值得總的回顧一個這些特性以及如何使用B-Tree索引。
在選擇索引和編寫利用這些索引的查詢時,有如下三個原則始終需要記住:單行訪問是很慢的。特別是在機械硬盤存儲中(SSD的隨機I/O要快很多,不過這一點仍然成交)。如果服務器從存儲中讀取一個數據塊只是爲了獲取其中一行,那麼就浪費了很多工作。最好讀取的快中包含儘可能多所需要的行。使用索引可以創建位置引用以提升效率。
按順序訪問範圍數據是很快的,這有兩個原因。第一,順序I/O不需要多次磁盤尋道,所以比隨機I/O要快很多(特別是對機械硬盤)。第二,如果服務器能夠按需要順序讀取數據,那麼就不再需要額外的排序操作,並且GROUP BY查詢也無須再做排序和將按組進行聚合計算了。
索引覆蓋查詢是很快的。如果一個索引包含了查詢需要的所有列,那麼存儲引擎就不需要再回表查找行。這避免了大量的單選訪問,而上面的第上點已經寫明單選訪問是很慢的。
總的來說,編寫查詢語句時應該盡選擇合適的索引以避免單選查找、儘可能地使用數據原生順序從而避免額外的排序操作,並儘可能使用索引覆蓋查詢。這與“三星”評價系統是一致的。