MySQL高級 —— 高性能索引

引言

最近一直在抱着《高性能MySQL(第三版)》研究MySQL相關熱點問題,諸如索引、查詢優化等,這階段的學習是前一段時間MySQL基礎與官方的“閱讀理解”的進一步延伸。

書中第五章詳細闡述瞭如何設計高性能的索引,以及索引的諸多注意事項。

之前關於索引的學習,包括《MySQL 高級 —— 複合索引簡介(多列索引)》、《MySQL 高級 —— 索引實現的思考》、《MySQL 優化 —— MySQL 如何使用索引》這三篇文章,着實費了一番功夫,但隨着對書中第五章內容的逐步學習,也越發覺得自己關於索引的理解和學習還不夠充分,遂以此篇《高性能索引》作以總結,本篇博客可能會與之前的這三篇有部分內容重合,重合的部分,我會一筆帶過或做一些必要的總結,多數內容都是新領悟到的知識。未來如果還有索引的新認識,應該不會再開新的文章了,而是會在這些文章的內容之上,進行更新,以免造成整體博客的質量下降。

一、索引的常識

索引是存儲引擎層實現的一種數據結構,因此,索引的實現非常依賴於具體的存儲引擎。

索引的三大優點

1、大大減少了服務器需要掃描的數據量
2、幫助服務器避免排序和臨時表
3、索引可以將隨機IO變爲順序IO

評價索引優劣的三星標準

1、索引將相關記錄放到一起可以獲得一星
2、索引中的數據順序可以滿足排序需求則獲得二星
3、索引中的列包含了查詢中需要的全部列可獲得三星。能夠獲得此三星的索引,也叫"三星索引"。

二、B樹索引

大多數場景的索引,都是指 InnoDB 存儲引擎中的 B+Tree索引,這是最常見的索引。

詳細的B樹結構分析可以參考《MySQL 高級 —— 索引實現的思考

在學習B-Tree 索引的時候,重點是要理解 B+Tree這種數據存儲結構,它的很多特性都是由這種特定的存儲結構決定的。爲什麼會有最左前綴原則?爲什麼索引可以直接排序數據?這些特性都要求我們深入瞭解 B+樹詳細存儲結構,明白非葉子節點和葉子節點的差別,明白葉子節點中存儲的內容等等,只有深入瞭解了B+Tree的存儲結構,才能更容易地記住各種複雜的索引特性,和索引使用限制。

B-Tree對索引列是順序存儲的即索引的節點是有序的。它的另一個重要特點是,或者說與基本的B樹索引的區別是,每個葉子節點都包含指向下一個葉子節點的指針(實際上是一個雙向指針),葉子節點之間可以看做是一個有序鏈表的結構,這是爲了方便葉子節點的範圍遍歷。

三、複合索引(或叫多列索引)

參考《MySQL 高級 —— 複合索引簡介(多列索引)

多列索引支持的查詢類型:

1、全列匹配
2、最左前綴列匹配
3、最左前綴列的最左前綴匹配:比如,idx_name_birth(last_name, first_name, birthday) 可以幫助查找last_name的前綴字符,比如last_name以'A'開頭。
4、最左前綴列的範圍匹配。
5、索引覆蓋查詢:只訪問索引的查詢,而無需訪問數據行。

前四點,主要是圍繞B+Tree索引的"最左前綴原則"

另外,索引的另一個重要用途是排序。如果 ORDER BY 子句滿足前面列出的幾種查詢類型,那麼索引也是可以直接排序數據的。

對於最左前綴原則,有一個小技巧,因爲某些原因,查詢中可能不會涉及到索引中靠前的列,那麼可以使用 IN()函數,將左前綴的列變爲多個等值條件,以符合最左前綴原則,避免讓索引失效。但這種方法不適合可選列表太大的情況。

四、哈希索引

哈希索引是基於哈希表實現,只能精確匹配索引所有列的查詢纔有效。因此使用範圍非常有限,但如果確定適合哈希索引,那麼性能將會有質的飛躍。存儲引擎會爲每行數據對應的索引列計算一個哈希碼,哈希碼是一個較小的值,哈希索引保存哈希碼和指向每個數據行的指針。

在MySQL中,只有Memory存儲引擎顯式支持哈希索引,是默認索引類型。

Memory引擎支持非唯一哈希索引,如果多個列的哈希值相同,索引會以鏈表的方式存放多個記錄指針到同一個哈希碼下。

哈希索引的結構:

槽(slot) 值(value)
2323     指向第1行指針
2458     指向第4行指針
7437     指向第100行指針
...      ...

哈希索引的結構本身也是一種key-value 結構,key就是索引列的哈希碼(哈希碼通過一個哈希函數來生成),值就是指針。在哈希索引中,保存key的結構成爲slot——槽,由於哈希碼是整數,所以槽是有序的,但指針是無序的

4.1 哈希索引在查詢時的工作過程

假設下面的查詢中 fname 列上有哈希索引:

SELECT ... FROM testhash WHERE fname = 'Peter';

那麼上面的查詢在執行時,MySQL會先計算'Peter'的哈希碼,然後在哈希索引中找到對應的哈希碼,以及其對應的行指針,最後比較行中的索引值是否等於'Peter'。

4.2 哈希索引爲什麼快?

哈希索引的工作原理非常簡單,因爲本身只存儲整數型的哈希值,其索引結構非常緊湊,因此命中目標的速度非常快。

4.3 哈希索引的缺點(限制)

1、哈希索引本身並不存儲行數據,所以不可避免地需要讀取行數據。不過這點對性能影響不大。
2、哈希索引無法用於排序
3、哈希索引不支持部分索引列匹配查找。記住,哈希索引始終是使用索引列的全部內容來計算哈希值的。
4、哈希索引只支持等值比較。提示,IN()也是等值比較。
5、哈希衝突,性能下降。哈希衝突是影響哈希索引性能的關鍵因素,發生哈希衝突的行的指針,會以鏈表的形式存儲於同一個哈希碼之下,因此存儲
引擎需要逐行比較才能最終確定結果,因此,哈希衝突越多,哈希索引性能越低。
6、哈希衝突,維護成本更高。當刪除數據行或者增加數據行時,如果發生哈希衝突,那麼就需要遍歷衝突行指針鏈表,增加維護成本。

4.4 InnoDB與哈希索引

InnoDB引擎有一個叫做"自適應哈希索引"的特殊功能。當InnoDB發現某個索引值使用非常頻繁時,會在內存中基於B-Tree索引之上再創建一個哈希索引。不過這個功能是完全自動的,用戶無法配置或控制,但可以關閉。

4.5 自定義哈希索引

我們可以自定義一種"僞哈希"索引。

其主要應用場景是:爲長字符串(如url)創建很小的哈希索引,既節省索引存儲空間,又能更快查詢。如果有針對URL進行搜索的需求,那麼非常適合建立這種"僞哈希索引"。

經典案例

某InnoDB表有一個url字段,如果正常對該列索引,並不是一個很明智的選擇,因爲url一般都很長,且毫無順序。

改造方法是,在表中建立一個 url_crc 列,該列用於存儲 url 列的由MySQL內建函數 CRC32()生成的哈希碼,那麼查詢的時候只需:

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

這樣做性能會非常高。但其實仔細觀察可以發現這種僞哈希索引的本質,其實就是在表中保存url的哈希碼,用B-Tree再去索引哈希碼。

但注意,查詢需要同時指定哈希碼和原列值,這是爲了僞哈希索引模擬哈希索引內部解決哈希衝突的操作

另外,哈希列做索引還需要注意維護工作,一般可以使用觸發器插入或更新時自動維護哈希列值

哈希值避免使用SHA1()或MD5()作爲哈希函數。因爲這兩個函數計算出來的哈希值是非常長的字符串。它們都是強加密函數,設計目的就是最大限度消除衝突,但這裏並不需要這麼高的要求。簡單哈希函數的衝突在一個可接受的範圍內就可以。

五、高性能索引策略

5.1 過濾條件中要使用獨立的索引列

索引列不能是表達式的一部分,也不能是函數的參數,諸如:WHERE act_id + 1 = 5;這樣的形式,act_id 是無法使用索引。要養成簡化WHERE子句的習慣,始終將索引列單獨放在比較符的一側。

5.2 前綴索引

當需要索引很長的字符串時,可以考慮前綴索引、僞哈希索引的設計組合。這樣做的時候,需要考慮索引空間以及索引選擇性的問題。具有較好選擇性的前綴索引,可以在存儲空間和性能之間尋找平衡點,簡言之,要選擇足夠長的前綴以保證較高的選擇性,同時又不能太長(節約空間)

什麼是索引的選擇性

基數(即可選值)和數據表記錄總數的比值。範圍是0到1,唯一索引的選擇性是1,性能最好。

前綴的長度選擇(平衡點),有兩種考量方式:

1、前綴重複次數,接近於全字符串重複次數的時候。

2、直接計算選擇性(平均選擇性)。如:

SELECT  COUNT(DISTINCT city)/COUNT(*) AS origin,
        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 city;

但注意,第二種方法計算的是平均選擇性,必須要在幾個候選項上,結合第一種方法,觀察具體重複的頻率是否真的接近原字符串。

另外,前綴索引無法用於排序或分組,也無法做索引覆蓋掃描。這都是因爲索引中存儲的是原字符串的前綴而不是原字符串。前綴索引的常見應用是針對十六進制唯一ID,一般採用長度爲8的前綴索引通常能夠顯著提升性能。

5.3 優先使用多列索引

大多數情況,多列索引(複合索引)的性能要優於單列索引。

5.4 合適的索引順序

定義索引(B-Tree索引)時索引列的先後順序很重要。一般的法則是:將列值選擇性高的索引列放在靠前位置。這個法則非常適用於優化WHERE 條件的查找。但也需要結合值的分佈(即具體值)進一步考量。這和前綴索引需要考慮的問題類似。可能還需要根據那些頻率最高的查詢來調整索引列的順序。

具體操作的方法是統計一下指定常量對應的數量:

SELECT SUM(stuff_id = 2), SUM(customer_id = 584) FROM tb;

當觀察到對應的重複數量後,應該將重複更少的列放到多列索引靠前的位置,但由於上面的統計很依賴於具體的值,所以在此之前還需要考察一件事,即平均選擇性(參考前面的總結),例如:

SELECT 
    COUNT(DISTINCT staff_id) / COUNT(*) AS staff_id_selectivity,
    COUNT(DISTINCT customer_id) / COUNT(*) AS customer_id_selectivity,
    COUNT(*)
FROM tb;

只要結合這兩方面考量,大多數情況下的查詢,索引列的順序都是比較合適的。

5.5 聚簇索引

這是一種數據存儲方式。用B-Tree結構同時保存索引和數據行。聚簇索引中,數據行保存在索引的葉子節點中。
因爲聚簇索引會直接用於存儲數據行全部數據,所以一個表只能有一個聚簇索引(但覆蓋索引可以模擬多個聚簇索引)。葉子節點包含數據行全部數據,普通節點只包含索引。

在MySQL中只支持主鍵的聚簇索引。因此InnoDB會建議用戶定義主鍵來做聚簇索引,如果表中沒有主鍵,InnoDB會選擇一個非空且唯一索引代替,如果也沒有,InnoDB會隱式地定義一個主鍵來做聚簇索引。

聚簇索引的優點:

1、數據訪問更快。

2、使用覆蓋索引掃描的查詢可以直接使用葉子節點中的主鍵值。 

聚簇索引的缺點

1、插入速度嚴重依賴於插入順序。按照主鍵順序插入是加載數據到InnoDB表中速度最快的方式。

2、更新聚簇索引列的代價很高。因爲會強制InnoDB將每個被更新的行移動到新的位置。

3、基於聚簇索引的表在插入新行,或者主鍵被更新需要移動行的時候,可能會出現"頁分裂"的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,也分裂會導致表佔用更多的磁盤空間。

4、聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏的時候,或者由於頁分裂導致數據存儲不連續的時候。

5、二級索引(聚簇索引表的非聚簇索引)可能比想象的要更大,因爲在二級索引的葉子節點中包含了引用行的主鍵列。

6、二級索引需要兩次索引查找。二級索引中保存的"行指針"的本質,是主鍵值,而不是數據行的物理地址指針。對於InnoDB,自適應哈希索引能夠減少這樣的重複工作。

5.6 覆蓋索引

MySQL可以通過索引來獲取列的數據。覆蓋索引就是旨在直接從索引中獲取數據,從而避免讀取行,極大地提升查詢的性能

如果一個索引包含(或者說覆蓋)所有需要查詢的字段值,我們就稱之爲——覆蓋索引

覆蓋索引的優點:

 

 

 

 

 

 

 

 

 

 

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