1. 程式人生 > >讀薄《高效能MySql》(三)索引優化

讀薄《高效能MySql》(三)索引優化

讀薄《高效能MySql》(一)MySql基本知識
讀薄《高效能MySql》(二)Schem與資料優化
讀薄《高效能MySql》(三)索引優化

#1 基礎知識

為了看懂這一篇博文,請先看懂 B+ 樹。因為 MySql 中大多數的引擎都是用這個資料結構作為索引的,特別是 InnoDB,因為基本上絕大多數的應用都是這個引擎,所以如果你有 10 份時間,花 9 份時間在這個引擎上也是沒錯的,本文後面要討論的內容也大多數是基於這個型別的索引的。

我們要注意這一個特性,就是 B+ 樹只有葉子節點才儲存資料(也就是在資料庫中指向某一行的指標),知道這個特性對於理解後面的內容非常重要。比如說主鍵要儘量的小,這樣可以一次性讀入更多的索引來查詢資料,

BTW,這裡有一個組成原理的知識需要大家掌握。對於機械硬碟來說,順序的讀取比隨機讀取快非常多。

機械硬碟長這樣:

讀取資料的時候,需要把磁頭移動到相應的柱面上然後通過磁碟旋轉來讀取這一個柱面的資料。

讀取時間 = 移動磁頭 + 磁碟旋轉讀取 + 傳輸時間

期中移動磁頭佔絕大部分時間,如果順序的讀取的話,就只需要移動一次磁頭,速度自然就提上去了。

隨機讀取,每讀一塊都要重新移動磁頭到相應的位置上去,速度就慢很多。

#2 明確目標

對於資料庫查詢優化,一個好的索引能讓操作快上好幾個數量級。索引的優點有

  1. 減少伺服器需要掃描的資料量
  2. 幫助伺服器避免排序和臨時表
  3. 將隨機 IO 變為 順序 IO

一個好的索引應該具備以下三個優點。

  1. 索引將相關記錄放在一起
  2. 索引中的順序和查詢順序(比如 ORDER BY)操作一致
  3. 索引包含了需要查詢的全部內容

書上把滿足這幾條需求的索引稱作”三星索引“,在後面三星索引一章會介紹這幾條原則。

#3 B+ 樹索引

#3.1 如何最大化利用索引

B+ 樹索引只能高效的使用最左字首,我們拿下面的索引舉例:

key(姓,名,生日)

只有以下的情況會用到索引:

全值匹配

全值匹配指的是匹配用到全部索引值,比如匹配姓為張,名為三,生日為 1998-11-11 的人,就可以用到索引。

WHERE 姓='張' AND 名='三' AND 生日='1998-11-11'

匹配最左字首

可以匹配到姓為張的全部人,也就是用到索引的第一列

WHERE 姓='張'

匹配列字首

比如裡面有英文名字,可以匹配到所有姓是以 A 開頭的人。

精確匹配到某一列並且範圍匹配到另一列

可以使用從索引的左邊到右邊任意的數量的字首,比如可以用一個半索引,查詢用

WHERE 姓='張' AND like '三%'

在上面的情況除了按值查詢能用到索引,ORDER BY 操作也能用到索引。

最左字首匹配也就是匹配需要從最左邊開始,無論 key 取多少位,一位,一位半,兩位都行。上面的索引不能用到查詢生日為某一天的人,因為沒有用到前面的姓和名,也就是沒有從最左邊開始,直接從第三列開始了。

B-Tree 索引有如下幾個限制

只能從索引的最左列開始查詢

比如說上面的例子無法查詢特定名的人,也不能查詢特定生日的人

不能跳過字首

上面的例子中,不能查詢特定的並且生日為某一天的人,因為跳過了中間的索引

當前面的key用到範圍查詢後,後面的查詢都不能使用索引查找了

比如說,不能查詢這樣的語句

WHERE 姓 LIKE 'A%' AND ...

在前面使用過範圍查詢後,後面的資料都不能通過索引找出了。

從上面的限制得出幾個比較重要的優化點:

  • 儘量避免多個範圍匹配在同一個查詢中出現
  • 索引的順序對於搜尋至關重要

#4 聚簇索引

什麼是聚簇索引?對於這棵 B+ 樹來說,如果不是採用的聚簇索引(比如在 MyISAM 引擎中),那些 Data 儲存的是索引對應的列的指標,也就是說,如果你想要訪問列的資料還需要根據指標查詢到那一列然後才能獲取資料。而對於聚簇索引來說,儲存的是列的資料,在讀入索引的時候可以直接將這一列的值讀入。

聚簇指的是資料和 B+ 樹的葉子節點緊湊的儲存在一起。因為無法把資料放在兩個地方,所以一張表只有一個聚簇索引。一般來說被索引的列是主鍵,如果沒有主鍵,會隱式的生成一個主鍵作為索引。

聚簇索引的實現是在引擎層面上的,這裡討論的是 InnoDB,原理對於其他的同樣引擎適用。

#4.1 聚簇索引優點

可以把相關資料儲存在一起,比如按照使用者 id 取郵件,因為郵件都根據 id 儲存在一個地方,所以只需要比較少次 I/O就可以把同一個使用者的全部郵件讀取完。

#4.2 聚簇索引缺點

  • 聚簇資料查詢順序依賴於插入順序,按照主鍵順序插入是最快的,如果不是按照主鍵順序插入,在插入後最好用 OPTIMIZE TABLE 重新組織一下表。

  • 更新聚簇索引列的代價很高,因為每個更新的行都要移動到新位置。

  • 需要插入行或者移動行的時候可能會產生碎片,會佔用更多空間。

  • 聚簇索引會導致全表掃描變慢,由於頁分裂會導致資料儲存不連續

#4.3 避免隨機插入

最好避免隨機的聚簇索引,這種情況下還不如使用一個和業務無關的自增列。特別是對於 I/O 密集操作,因為隨機索引插入,這會導致頁分裂並且需要移動列的位置。

#5 高效能索引

#5.1 查詢中索引不要帶表示式

查詢條件中,索引不能是表示式的一部分,也不能是函式的引數,MySql 並不會懂得去優化它,比如說下面的選擇。

SELECT id from singer where id + 1 = 5

我們很容易看出來這個表示式等價於查詢 id 為 4 的歌手,但是 MySql 不懂得自己去優化它,不會使用到索引,而是採用全表搜尋。

#5.2 選擇合適索引字首長度

我們首先引入個概念

索引的選擇性:通俗地說就是索引對列的區分度,比如說如果每一個列有唯一的 id,那麼選擇性就是 1,如果有 100 個相同的 id,那麼選擇性就是 1 / 100,索引的選擇性越高效能越好。

有時候索引需要很長的字串,這些索引會讓查詢變得很慢,一個解決策略就是前面說到的手動建立雜湊索引。另外一種方法就是隻使用前面幾個字元來作為索引,但是這樣會降低索引的選擇性。訣竅在於要使用剛剛好長度的字串字首作為索引,索引太長會導致一次性讀入記憶體的索引減少,太短會導致區分度不高,B+ 樹查詢完索引還要線性查詢很長一段距離。

為了更直觀的解釋如何選擇合適的長度,我們拿一張表來舉個例子

song(id, title)

歌曲表,裡面有 id 和 標題,我們的目標是在 title 上建立索引。

為了直觀的看出索引的區分度,我們可以用一些 SQL 語句來測試資料,逐漸增加字首的長度,選擇最適合的長度。

SELECT COUNT(*) AS cnt , LEFT(title,3) AS pref FROM song GROUP BY pref ORDER BY cnt DESC LIMIT 10;

選擇結果如下

我們只看數值最大的那個,因為如果第一個數值很大,那麼查詢這個字首的時候效率會變得很低,我們可以慢慢增加字首長度直到數字達到可以接受的數值。

當然也可以計算一個查詢的字首長度的選擇性

SELECT COUNT(DISTINCT LEFT(title, 3))/COUNT(*) AS sel3,
COUNT(DISTINCT LEFT(title, 4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(title, 5))/COUNT(*) AS sel5
FROM song;

這個值越大查詢效率越高,一般來說,可以慢慢增加字首直到這個數值增加的不明顯為止,然後根據具體需求選擇出合適的長度。

#5.3 避免多列索引

在上一節的例子中,有人可能會建立這樣的索引。

key(名),key(姓),key(生日)

這樣執行下面這個查詢的時候,可能用不到這個索引。

SELECT * FROM student WHERE 名='寧' AND 姓='寧'

對於這樣的查詢,這個索引

key(姓,名)

會更合適

在 MySql 5.0 和更新版本中,查詢能同時使用兩個索引進行掃描,並將結果合併(這就用到了這 key(姓)key(名) 這兩個索引)。索引合併是一項優化策略,但是更可能說明了索引建立的很糟糕。

當出現了多個 AND 關聯的查詢,應該採用包含所有類的索引而不是獨立的單個索引。

key(姓, 名)

#5.4 選擇合適的索引列順序

從前面講到的索引限制條件,我們可以知道,一個好的索引順序能極大的提高索引的利用率。

當不需要排序和分組的時候,將選擇性最高的列放在前面通常是很好的選擇,這是一種經驗法則。但是這和值的分佈有關,也和業務有關,沒有一個四海皆准的法則。

一個比較有效的方法就是提取出比較差的查詢,然後按照選擇率來建立索引,將選擇性高的列放在索引左邊建立索引。

#5.5 索引覆蓋

當使用到的索引就是需要的值,那麼就能減少磁碟的 IO,查詢效率大大提升。

比如說有這個索引 key(sex, age),當查詢性別男,生日為某一天的時候,匹配到索引的時候就直接返回結果就行了 ,不需要再進行磁碟 IO 來拿出這一整列。

而且因為索引是按照列值順序儲存的,所以對於 I/O 密集的範圍查詢,會比隨機從磁碟讀取每一行資料的 I/O 少得多。

#5.6 使索引與排序順序一致

MySql 有兩種方式來生成排序,一種是直接利用索引掃描順序作為排序,一種是通過排序操作。當 EXPLAIN 出來的 type 的值為 index 時說明使用了索引進行排序。

設計索引的時候要儘量使得索引的列順序和 ORDER BY 順序一致。

只有當索引的順序和 ORDER BY 的子句一致,並且所有的列排序方向都一樣的時候,MYSQL 才能用索引來對結果進行排序。如果需要不同的方向進行排序,一個好的方法就是在儲存的時候翻轉字串或者儲存相反數。當需要關聯查詢多個表的時候,只有 ORDER BY 子句引用的欄位全為第一個表的時候,才能用索引做排序。

索引在 Order By 中的使用限制也和 SELECT 中的限制一樣

#6 索引和鎖

InnoDB 訪問行的時候才會鎖定這行,索引可以讓查詢訪問更少行,從而鎖定更少行。

然而索引是按照順序自增的時候,併發插入會導致行鎖的競爭。

在舊版中,需要提交事務才能釋放行鎖,在 MySql 5. 1 後的版本,InnoDB 可以在服務端過濾掉行後就釋放鎖。

#7 清除冗餘和重複索引

MySql 允許重複索引的存在,並且需要單獨維護重複的索引。有時候我們需要檢視未使用到的索引。

可以用 Percona Toolkit 來檢視索引情況和查詢索引的日誌來找出重複和多餘的索引。

接下來討論一下冗餘索引,冗餘索引一般是增加索引(A,B)的時候,而不是去擴充套件已有的索引(A)。

大多數情況下都不需要冗餘索引。除非當需要額外增加一個非常長的 VARCHAR 索引的時候,增加這個列會導致效能急劇下降,這時候就需要冗餘索引。

#8 一些其他的索引討論

順帶一提,如果有特殊需要,可以使用其他的索引。

R-Tree:可以用任意維度進行索引,MySql 在這方面做得不好,可以考慮使用 PostgreSQL

全文索引:用於搜尋,一般可以將整張表匯入 elasticsearch 中。

#8.1 雜湊索引

在 MySql 中,只有 Memory 引擎支援顯式雜湊索引,它是支援非唯一雜湊索引的。也就是說類似於 Java 中的 HashMap(JDK 1.8 前),雜湊到同一個位置的列會被儲存在連結串列中。

如果資料庫不支援雜湊索引,可以在 B-Tree 中建立一個偽雜湊索引,比如需要儲存大量的 URL,可以在 WHERE 語句中利用特定的方法來把 URL 雜湊開來,可以藉助 MySql 外掛來做到這一點。這樣的缺點就是需要使用觸發器在列表更新的時候更改雜湊值。