MySQL系列-- 3.建立高性能的索引

3.建立高性能的索引

  • 索引是存儲引擎用於快速找到記錄的一種數據結構。
  • 對良好的性能很是關鍵。
  • 對查詢性能優化最有效的手段。輕易將查詢性能提升幾個數量級。

3.1 索引基礎

  • 存儲引擎使用索引,先在索引中找到對應的值,而後根據匹配的索引記錄找到對應的數據行。
  • 能夠包含一個或多個列的值。若是包含多個列,那麼列的順序也很重要,由於MySQL只能高效地使用索引的最左前綴列。

3.1.1 索引的類型

索引是在存儲引擎層而不是服務器層實現的,因此沒有統一的索引標準:不一樣存儲引擎的索引的工做方式不同,也不是蓑鮋的存儲引擎都支持全部類型的索引。即便多個存儲引擎支持同一種類型的索引,其底層的實現也可能不一樣。mysql

  • B-Tree索引:算法

    • 實際上不少存儲引擎使用的是B+Tree,即每個葉子節點都包含指向下一個葉子節點的指針,從而方便葉子節點的範圍遍歷。sql

    • 對索引列是順序組織存儲的,很適合查找範圍數據。數據庫

    • 索引對多個值進行排序的依據是CREATE TABLE語句中定義索引時列的順序。緩存

    • MyISAM使用前綴壓縮儘速使得索引更小,並經過數據的物理位置引用被索引的列;InnoDB則按照原數據格式進行存儲,並根據主鍵引用被索引的行。性能優化

    • 優化性能的時候,可能須要使用相同的列但順序不一樣的索引來知足不一樣類型的查詢需求。服務器

    • B+Tree結構示例:數據結構

      B+Tree結構
      B+Tree結構

    • 意味着全部的值都是按順序存儲的,而且每一個葉子頁到跟的距離相同。其中葉子節點的指針指向的是被索引的數據,而不是其它的節點頁。以下爲一個節點和其對應的葉子節點示例圖,其實在根結點和葉子結點之間可能有不少層節點頁,樹的深度和表的大小直接相關。併發

      • 從索引的根結點(並未畫出)開始搜索,於是存儲引擎再也不須要進行全表掃描,加快訪問數據的速度。
      • 根結點的槽中存放了指向子節點的指針,存儲引擎根據這些指針向下查找
      • 經過比較節點頁的值和要查找的值能夠找到合適的指針進入下層子節點,這些指針實際上定義了子節點頁中值的上限和下限。
      • 最終找到對應的值,要麼記錄不存在。

      創建在B-Tree結構(從技術上來講是B+Tree)上的索引
      創建在B-Tree結構(從技術上來講是B+Tree)上的索引

    • B-Tree索引的查詢類型(適用於全鍵值、鍵值範圍或鍵前綴查找,其中鍵前綴查找只適用於根據最左前綴的查找):函數

      • 全值匹配:對索引中全部的列進行匹配
      • 匹配最左前綴:匹配最左索引的列
      • 匹配列前綴:匹配某一列的值的開頭部分,需包含最左列
      • 匹配範圍值:匹配某一列值的某一特定範圍,需包含最左列
      • 精確匹配某一列並範圍匹配另一列,需包含最左列
      • 只訪問索引的查詢:查詢只須要訪問索引,而無須訪問數據行。
    • 除了按值查找外,還能夠用於查詢中的ORDER BY進行排序操做(按順序查找)。若是ORDER BY知足以前的查詢類型,也能夠知足對應的排序需求。

    • 限制:

      • 若是不是按照索引的最左列開始查找,則沒法使用索引。
      • 不能跳過索引的列
      • 若是查詢中有某個列的範圍查詢,如LIKE,則其右邊全部列都沒法使用索引優化查找。
  • 哈希索引:

    • 基於哈希表實現,只有精確匹配索引全部列的查詢纔有效。對於每一行數據,存儲引擎都會對全部的索引列計算一個哈希碼(hash code),哈希碼是一個較小的值,而且不一樣鍵值的行計算出來的哈希碼也不同。哈希索引將全部的哈希碼存儲在索引中,同時在哈希表中保存指向每一個數據行的指針。

    • MySQL中,只有Memory引擎顯示支持哈希索引,也是該引擎表的默認索引類型,也支持B-Tree索引,另外還支持非惟一哈希索引(若是多個列的哈希值相同,索引會以鏈表的方式存放多個記錄指針到同一個哈希條目中)。

    • 索引自身只需存儲對應的哈希值,因此索引的結構十分緊湊,也讓哈希索引查找的速度很是快。

    • 限制:

      • 只包含哈希值和行指針,而不存儲字段值,因此不能使用索引中的值來避免讀取行。但因爲在內存中,對性能的影響並不明顯。
      • 並非按照索引值順序存儲的,因此沒法用於排序。
      • 不支持部分索引列匹配查找,由於哈希索引始終是使用索引列的所有內容來計算哈希值。
      • 只支持等值比較查詢,包括=、IN()、<=>。也不支持任何範圍查詢。
      • 訪問哈希索引的數據很是快,除非有不少哈希衝突(不一樣索引列值卻有相同的hash值)。當出現哈希衝突時,存儲引擎必須遍歷全部的行指針,逐行比較,直到找到對應的行。
      • 若是哈希衝突不少的話,一些索引維護操做的代價也很是高。例如刪除一行時,需遍歷對應哈希值的每一行。
    • 應用場景:

      • 數據倉庫應用中有一種經典的「星型」schema,須要管理不少查找表
    • InnoDB的自適應哈希索引(adaptive hash index):

      • 當InnoDB注意到某些值被使用的很是頻繁時,會在內存中基於B-Tree索引之上在建立一個hash索引,從而擁有hash索引的優勢,如快速的hash查找。這是一個徹底自動的,內部的行爲,用戶沒法控制或者配置,但能夠關閉。
    • 建立自定義哈希索引:

      • 若是存儲引擎不支持hash索引,能夠模仿像InnoDB同樣建立hash索引。

      • 思路:在B-Tree基礎上建立一個僞哈希索引,即將要索引的列刪除索引,對其建立一個被索引哈希列,裏面存放原索引列每一行數據的哈希值。

      • 缺陷:須要維護哈希值,能夠手動維護,也可使用觸發器實現。

      • 使用時不要使用SHA1()和MD5()做爲哈希函數,由於這兩個函數計算出來的哈希值很是長,浪費大量空間並且比較時也會更慢。可使用CRS32()。

      • 若是數據表很是大,CRS32()會出現大量的數據衝突,能夠自行實現一個簡單的64位哈希函數,這個函數要返回整數,而不是字符串。也可使用MD5()函數返回值的一部分來做爲哈希函數。

        SELECT CONV(RIGHT(MD5("str"), 16), 16, 10) AS HASH64

      • 使用哈希索引進行查詢時,必須在WHERE子句包含對應列值,由於可能會有哈希衝突從而選出多個不一樣的數據。

  • 空間數據索引(R-Tree):

    • 與B-Tree不一樣,這類索引無需前綴查詢,會從全部維度來索引數據。
    • 查詢時可使用任意維度來組合查詢。
    • 必須使用MySQL的GIS相關函數如MBRCONTAINS()等來維護數據。(MySQL的GIS支持不完善,開源關係數據庫中較好的解決方案是PostgreSQL的PostGIS)
    • MyISAM表支持空間索引,能夠用做地理數據存儲
  • 全文索引:

    • 查找的是文中關鍵詞,而不是直接比較索引中的值。
    • 與其餘索引徹底不同,需注意如停用詞、詞幹和複數、布爾搜索等細節。
    • 更相似於搜索引擎作的事,而不是簡單的WHERE條件匹配。
    • 在相同列上同時建立全文索引和基於值的B-Tree索引不會有衝突,全文索引適用於MATCH AGAINST操做。

3.2 索引的優勢

  • 優勢:
    • 大大減小服務器須要掃描的數據量
    • 幫助服務器避免排序和臨時表(B-Tree會將相關的列值存儲在一塊兒,便於ORDER BY 和GROUP BY進行排序)
    • 能夠將隨機IO變爲順序IO
  • 索引適合某個查詢的「三星系統」:
    • 將相關記錄放到一塊兒則得到一星
    • 索引中的數據順序和查找中的排列順序一致得到二星。
    • 索引中的列包含了查詢中須要的所有列得到三星。
  • 索引並非最好的解決方案:
    • 很是小的表,大部分狀況下全表掃描更高效。
    • 中到大型的表,索引很是有效。
    • 特大型的表,創建和使用索引的代價隨之增加,須要區分出查詢須要的一組數據,如分區技術。
    • 表的數量特別多,能夠創建一個元數據信息表,用來查詢須要用到的某些特性。例如執行那些須要聚合多個應用分佈在多個表的數據的查詢,則須要記錄「哪一個用戶的信息存儲在哪一個表中」的元數據,這樣在查詢時就能夠直接忽略掉那些不包含指定用戶信息的表。對大型系統是一個經常使用的技巧
    • 對於TB級別的數據,定位單條記錄的意義不大,因此常用塊級元數據技術來代替索引

3.3 高性能的索引策略

3.3.1 獨立的列

  • 索引列不能是表達式的一部分,也不能是函數的參數。由於MySQL沒法自動解析

    WHERE column + 1 = 5 AND TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10

3.3.2 前綴索引和索引選擇性

索引很長的字符列,會讓索引變得大且慢。一個策略是模擬的哈希索引,另外一個策略是前綴索引。

  • 索引選擇性:
    • 指不重複的索引值(也成基數,cardinality)和數據表的記錄總數(#T)的比值
    • 選擇性越高則查詢效率越高,惟一索引的選擇性是1,性能是最好的。
  • 前綴索引:

    • 索引開始的部分字符串,便可節約索引空間,從而提升索引效率,但會下降索引的選擇性。

    • 通常狀況下某個列前綴的選擇性也是足夠高的,足以知足查詢性能。

    • 針對BLOB,TEXT或很長的VARCHAR類型的列,必須使用前綴索引,由於MySQL不容許索引這些列的完整長度。

    • 選擇前綴長度的訣竅:保持較高的選擇性(接近於索引完整列),同時又不能太長。換句話說,前綴的基數應接近於完整列的基數。

    • 方法:

      先計算出完整列的選擇性:SELECT COUNT(DISTINCT col)/COUNT(*) FROM mytable , 再與平均選擇性和最差選擇性比較。

      • 平均選擇性:在一個查詢中針對不一樣前綴長度進行計算

        SELECT COUNT(DISTINCT LEFT(col, 3))/COUNT(*) AS sel3,
            COUNT(DISTINCT LEFT(col, 4))/COUNT(*) AS sel4,
            ...
        FROM mytable;複製代碼
      • 最差選擇性:針對平均選擇性選出的多個前綴長度,考慮其數據分佈很不均勻下的選擇性。

    • 缺點:

      • MySQL沒法使用前綴索引作ORDER BY和GROUP BY
      • 沒法作覆蓋掃描
    • 應用場景:

      • 針對很長的十六進制惟一ID,如保存網站的會話(SESSION),可採用長度爲8的前綴索引,並且對上層應用徹底透明。
    • 有時使用後綴索引也有用途,如找到某個域名的全部電子郵件地址。但MySQL原生不支持反向索引,能夠經過觸發器把字符串反轉後存儲,並基於此創建前綴索引。

3.3.3 多列索引

  • 在多個列上創建獨立的單列索引大部分狀況下並不能提升MySQL的查性能。

    • MySQL5.0及之後版本引入「索引合併(index merge)"的策略,必定程度上可使用表上的多個單列索引來定位指定的行。對示例查詢同時使用兩個單列索引進行掃描,並將結果進行合併,能夠經過EXPLAIN的Extra看到過程。這算法有三個變種:
      • OR條件的聯合(union)
      • AND條件的相交(intersection)
      • 組合前兩種狀況的聯合及相交
    • 更早版本的MySQL只能使用其中的某一個單列索引,而這種狀況下沒有哪個獨立的單列索引是很是有效的。對示例查詢使用全表掃描,除非改爲UNION的方式。
    -- 兩個單列索引的查詢
    mysql> SELECT film_id, actor_id FROM sakila.film_actor
        -> WHERE actor_id = 1 OR film_id = 1;複製代碼
  • 索引合併策略有時候是一種優化的結果,但實際上更多時候說明了表上的索引建得很糟糕:

    • 當出現服務器對多個索引作相交操做時(一般有多個AND條件),一般意味着須要一個包含全部相關列的多列索引,而不是多個獨立的單列索引
    • 當服務器對多個索引作聯合操做時(一般有多個OR條件),一般須要耗費大量的CPU和內存在算法的緩存、排序和合並操做上。特別是當其中有些索引的選擇性不高,須要合併掃描返回的大量數據的時候。
    • 優化器不會把這些計算到「查詢成本(cost)」中,而優化器只關心隨機頁面讀取。這會使得查詢的成本被低估,致使執行該計劃還不如直接走全表掃描。這樣作不但會消耗更多的CPU和內存資源,還可能會影響查詢的併發性,但若是是單獨運行這樣的查詢每每會忽略對併發性的影響。一般來講,將查詢改成UNION的方式每每更好。
  • 若是在EXPLAIN中看到有索引合併,應檢查下查詢和表的結構以達到最優。也能夠經過參數optimizer_switch來關閉索引合併功能,或使用INGORE INDEX提示讓優化器忽略掉某些索引。

3.3.4 選擇合適的索引列順序(B-Tree場景)

正確的順序依賴於使用該索引的查詢,而且同時須要考慮如何更好地知足排序和分組的須要。

  • 選擇索引列順序的經驗法則:
    • 當不須要考慮排序和分組時,將選擇性最高的列放到索引最前列
    • 性能不僅是依賴於全部索引列的選擇性(總體基數),也和查詢條件的具體值有關,也就是和值的分佈有關。
      • 若是某些索引值的選擇性很是小,即匹配的範圍很是大,說明該索引基本沒什麼用。該特殊狀況可能會摧毀整個應用的性能。

3.3.5 聚簇索引(主要關注InnoDB)

  • 不是一種單獨的索引類型,而是一種數據存儲的方式。具體的細節依賴於其實現方式,但InnoDB的聚簇索引實際上在同一個結構中保存了B-Tree索引和數據行。

  • 當表有聚簇索引時,它的數據行實際上存放在索引中的葉子頁(leaf page)中,但節點也只包含了索引列。術語的「聚簇」表示數據行和相鄰的鍵值緊湊地存放在一塊兒(Oracle中爲索引組織表)。如圖,被索引的列是主鍵列

    聚簇索引的數據分佈
    聚簇索引的數據分佈

  • 一個表只能有一個聚簇索引,由於沒法同時將數據行存放在兩個不一樣的地方

  • MySQL內建的存儲引擎不支持選擇索引做爲聚簇索引,InnoDB將經過主鍵彙集數據,其默認使用聚簇索引:

    • 若是沒有定義主鍵,InnoDB會選擇一個惟一的非空索引。
    • 若是沒有惟一的非空索引,InnoDB會隱式定義一個逐漸。
  • InnoDB只彙集在同一個頁面的記錄,包含相鄰鍵值的頁面可能會相距甚遠。

  • 優缺點:

    • 優勢(設計表和查詢時充分利用可極大地提示性能):
      • 能夠把相關數據保存在一塊兒。如實現電子郵箱時,根據用戶ID來彙集數據,這樣只需從磁盤讀取少數的數據頁就能獲取某個用戶的所有郵件。若是沒有使用聚簇索引,則每封電子郵件均可能致使一次磁盤IO。
      • 數據訪問更快。由於索引和數據都保存在同一個B-Tree中。
      • 使用覆蓋索引掃描的查詢能夠直接使用頁節點中的主鍵值。
    • 缺點:
      • 最大限度地提升了I/O密集型應用的性能,但若是數據所有存放在內存中,則訪問的順序就沒那麼重要了,聚簇索引也就沒什麼優點。
      • 插入速度嚴重依賴於插入順序。按照主鍵的順序插入是加載數據到InnoDB表中速度最快的方式。若是不是按照主鍵順序加載數據,加載完成後最好使用OPTIMIZE TABLE命令從新組織一下表。
      • 更新聚簇索引列的代價很高,由於會強制InnoDB將每一個被更新的行移動到新的位置。
      • 基於聚簇索引的表在插入新行,或者主鍵被更新致使須要移動行的時候,可能面臨「頁分裂(page split)」的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,這會致使表佔用更多的磁盤空間。
      • 可能致使全表掃描變慢,尤爲是行比較稀疏,或者因爲頁分裂致使數據存儲不連續的時候。
      • 二級索引(非聚簇索引)可能比想象的要更大,由於在二級索引的葉子節點包含了引用行的主鍵列。
      • 二級索引訪問須要兩次索引查找,而不是一次。由於二級索引葉子節點保存的不是指向行的物理位置的指針,而是行的主鍵值。(InnoDB的自適應哈希索引可以減小這樣的重複工做)
  • InnoDB和MyISAM的數據分佈對比

    聚簇和非聚簇表對比圖
    聚簇和非聚簇表對比圖

    • InnoDB:
      • 因爲採用了聚簇索引,其保存了整個表
      • 聚簇索引每一個葉子節點都包含了主鍵值、事務ID、用於事務和MVVC的回滾指針以及全部的剩餘列。
      • 二級索引的葉子節點存儲的不是"行指針",而是主鍵值,並以此做爲指向行的「指針」。即葉子節點包含被索引的列和主鍵列。這樣的策略會讓二級索引佔用更多的空間,但減小了當出現行移動或者數據頁分裂時二級索引的維護工做,由於無須更新二級索引中的指針。
    • MyISAM
      • 採用了獨立的行存儲,按照數據插入的順序存儲在磁盤上
      • 主鍵索引和其餘索引在結構上同樣,主鍵索引是一個名爲PRIMARY的惟一非空索引。
  • 在InnoDB表中按主鍵順序插入行

    • 若是沒有數據須要彙集,建議定義一個代理鍵做爲主鍵,而且主鍵的數據應該和應用無關。最簡單是使用AUTO_INCREMENT自增列,這樣能夠保證數據行是按順序寫入的,對於根據主鍵作關聯操做的性能更好。

    • 最好避免隨機的(不連續且值的分佈範圍很是大)聚簇索引,特別是對於I/O密集型的應用,好比使用UUID做爲聚簇索引可能會帶來糟糕的性能,它使得聚簇索引的插入徹底隨機,使得插入行的時間更長,並且索引佔用的空間更大。由於主鍵的字段更長,還因爲頁分裂和碎片致使。

    • 根據順序id插入數據:

      每條記錄都存儲在上一條記錄的後面,當達到頁的最大填充因子時(InnoDB默認爲頁大小的15/16,留出部分空間用於之後修改),下一條記錄會插入新的頁中。一旦數據按照這種順序的方式加載,主鍵頁就會被近似於被順序的記錄填滿(二級索引頁多是不同的)

      形成更壞結果的場景:

      • 對於高併發工做負載,可能會形成明顯的爭用。由於全部的插入都發生在這裏,可能致使間隙鎖競爭。
      • AUTO_INCREMENT鎖機制也可能會被爭用,需考慮從新設計表或者應用,或者更改innodb_autoinc_lock_mode配置。
    • 使用隨機id插入數據:

      新行的主鍵值不必定比以前插入的大,所以須要爲新行找到合適的位置——一般是已有數據的中間位置——並分配空間。這會增長不少額外的工做,並致使數據分佈不夠優化。

      缺點:

      • 寫入的目標頁可能已經刷新到磁盤並從緩存中移除,或是尚未被加載到緩存中,InnoDB在插入以前需先從磁盤讀取目標頁到內存中,這將致使大量的隨機IO。
      • 由於寫入是亂序的,須要頻繁地作頁分裂操做,以便爲新行分配空間。由於頁分裂會致使移動大量數據,一次插入最少須要修改三個頁而不是一個頁。
      • 因爲頻繁的頁分裂,頁會變得稀疏並被不規則地填充,因此最終數據會有碎片。

3.3.6 覆蓋索引

設計優秀的索引應該考慮到整個查詢,而不僅僅是WHERE條件部分

  • 覆蓋索引:一個索引包含(覆蓋)全部須要查詢的字段的值

  • 查詢只須要掃描索引而無須回表讀取數據行的好處:

    • 索引條目一般小於數據行大小,若是隻須要讀取索引會極大地減小數據訪問量。這對緩存的負載很是重要,由於這種狀況下響應時間大部分花在數據拷貝上。覆蓋索引對IO密集型的應用也有幫助,由於索引被數據更小,更容易所有放入內存中(尤爲是MyISAM能壓縮索引)
    • 索引是按照列值順序存儲的(至少在單個頁內是如此 ),因此對於IO密集型的範圍查詢會比隨機從磁盤讀取每一行數據的IO要少得多。
    • 一些存儲引擎如MyISAM在內存中只緩存索引,數據則依賴於操做系統來緩存,所以訪問數據須要一次系統調用。這可能會致使嚴重的性能問題,尤爲是那些系統調用佔了數據訪問中的最大開銷。
    • 覆蓋索引對使用了聚簇索引的InnoDB的表很是有用。InnoDB的二級索引在葉子節點保存了行的主鍵值,因此若是二級節點可以覆蓋查詢,則能夠避免對主鍵索引的二次查詢。
  • 覆蓋索引必需要存儲索引列的值,而哈希索引、空間索引和全文索引都不存儲,MySQL只能使用B-Tree索引作覆蓋索引。

  • 沒法使用覆蓋索引的緣由:

    • 沒有任何索引可以覆蓋這個查詢。
    • 不能再索引執行LIKE操做。
  • 可使用延遲關聯使用覆蓋索引,由於延遲了對列的訪問。先在查詢第一階段使用覆蓋索引,再在外層查詢所要獲取的列值。

  • InnoDB的二級索引的葉子節點包含了主鍵的值,這意味着二級索引能夠有效地利用這些主鍵列來覆蓋查詢。

    -- last_name字段有二級索引,雖然該索引的列不包括逐漸actor_id,但也能用於對actor_id作覆蓋查詢
    mysql>EXPLAIN SELECT actor_id, last_name
         -> FROM sakila.actor WHERE last_name = "HOPPER"\G複製代碼
  • 使用InnoDB的表經過主鍵查詢全部列,並非覆蓋查詢,雖然聚簇索引的葉子節點包含了全部列的數據,但它只是一種數據存儲方式,並不算索引。

3.3.7 使用索引掃描來作排序

  • MySQL生成有序結果的方式:
    • 經過排序操做
    • 按索引順序掃描。EXPLAIN出來的type列的值爲「index」
  • 索引若是不能覆蓋查詢所須要的所有列,那每一條記錄都須要回表查詢。這基本上是隨機IO,比順序地全表掃描更慢,尤爲是在IO密集型的工做負載時。
  • 設計索引儘量知足排序和查找行。
  • 索引掃描排序的要求(如不知足都要執行排序操做):
    • 只有索引的列順序和ORDER BY子句順序徹底一致,而且全部列的排序方向(倒序或正序)都同樣,MySQL纔可以使用索引來對結果進行排序。
    • 若是查詢須要關聯多個表,只有當ORDER BY子句引用的字段所有爲第一個表時
    • ORDER BY子句和查找型查詢的限制是同樣的:需知足索引的最左前綴要求
  • ORDER BY子句能夠在前導列爲常量\常數的時候忽略該限制,若是WHERE或者JOIN子句對這些列定義了常量。... WHERE col1="xxx" ORDER BY col2 DESC;,其中col1和col2爲聯合索引。
  • 不能使用索引作排序的查詢:
    • ORDER BY使用了兩種不一樣的排序方向
    • ORDER BY引用了不在索引中的列
    • WHERE 和ORDER BY 中的列沒法組合索引的最左前綴
    • WHERE在第一列是範圍查詢,MySQL沒法索引其他列
    • 在某列上有多個等於條件,對排序來講也是範圍查詢。

3.3.8 壓縮(前綴壓縮)索引

  • MyISAM使用前綴壓縮來減小索引大小,從而讓更多索引能夠放入內存中,在某些狀況下能極大地提升性能。
  • 默認只壓縮字符串,經過設置也能壓縮整數。
  • 壓縮每一個索引塊的方法:先徹底保存索引塊的第一個值,而後將其餘值和第一個值比較獲得相同的前綴字節數和剩餘的不一樣後綴部分,再把這部分存儲起來。MyISAM對指針也採用相似的壓縮方式。
  • 壓縮塊使用更少的空間,代價是某些操做可能更慢。由於每一個值都依賴前面的值,沒法使用二分查找只能從頭開始掃描,而對倒序的掃描性能更差。
  • 對CPU密集型應用,由於掃描常常要隨機查找,不推薦使用該索引。
  • 在CREATE TABLE語句中制定PACK_KEYS參數來控制索引壓縮的方式。

3.3.9 冗餘和重複索引

  • 冗餘索引:在相同列上建立多個索引。MySQL須要單獨維護重複的索引,而且優化器在查詢時也須要逐個考慮,可能會影響性能。

    • (A)是(A,B)的冗餘索引,(B,A)和(B)則不是,只針對B-Tree索引來講
    • (A,ID)也是冗餘索引,由於對InnoDB主鍵列已經包含在二級索引中
    • 其餘類型的如哈希索引也不會是B-Tree的冗餘索引
    • 增長新索引會致使INSERT,UPDATE等操做的速度變得更慢,特別是新增索引達到了內存瓶頸的時候。
  • 重複索引:在相同列上按照相同順序建立的相同類型的索引。應該避免這種操做,常見錯誤作法是對一個主鍵添加惟一限制和查詢索引,這屬於三個重複的索引。(若是索引的類型不一樣,並不算重複索引)

  • 大多數狀況下都不須要冗餘索引,應該儘可能擴展已有的索引而不是建立新的索引。除非擴展已有的索引會致使其變得太大,從而影響其餘使用該索引查詢的性能。

    • 假如在整數列上有一個查詢,如今須要額外增長很長的VARCHAR列來擴展該索引,可能會致使性能急劇降低。特別是有查詢把這個索引看成覆蓋查詢,或者是MyISAM表而且有不少範圍查詢。

      -- Q1查詢:
      SELECT count(*) FROM userinfo WHERE state_id=5;
      -- Q2查詢:
      SELECT state_id, city, address FROM userinfo WHERE state_id=5;
      -- Q2的查詢速度會比Q1慢,最簡單的辦法是擴展索引變成覆蓋查詢:
      ALTER TABLE userinfo DROP key state_id, ADD KEY state_id_2 (state_id, city, address);
      -- 索引擴展後,Q2運行更快,但Q1變慢了。若是想要兩個查詢都變得更快,就須要兩個索引,儘管這是冗餘的。複製代碼
  • 解決冗餘和重複索引的辦法只須要刪除它們。找出這些索引的辦法:

    • 寫一些複雜的訪問INFORMATION_SCHEMA表的查詢(服務器若是有大量的數據或表,可能會致使性能問題)
    • 第三方工具。
  • 因爲二級索引包含了主鍵值,所以(A)至關於(A,ID),對WHERE A=5 ORDER BY ID這樣的查詢頗有用。但若是(A)擴展爲(A,B)至關於(A,B,ID),前面的查詢就沒法使用該索引排序,而只能用文件排序。

3.3.10 未使用的索引

找出它們,刪掉!不過有些索引的功能至關於惟一約束,雖然一直沒被查詢使用,可是是用於避免產生重複數據的。

3.3.11 索引和鎖

  • 索引能夠鎖定更少的行。若是查詢從不訪問那些不須要的行,那麼就會鎖定更少的行:
    • 雖然InnoDB的行鎖效率很高,內存使用也不多,可是鎖定行的時候仍然會帶來額外的開銷。
    • 鎖定超過須要的行會增長鎖爭用並減小併發性。
  • InnoDB只有在訪問行的時候纔會對其加鎖,而索引可以減小InnoDB訪問的次數,從而減小鎖的數量。但只有當InnoDB在存儲引擎層可以過濾掉全部不須要的行時纔有效。
    • 若是索引沒法過濾掉無效的行,那麼在InnoDB檢索到數據並返回給服務器層後,MySQL服務器才能應用WHERE子句。而這時候InnoDB已經鎖住了這些行(包含有沒被索引的行數據,這些是要在服務器層被過濾掉的,由於索引只在存儲引擎層工做),到適當的時候才釋放。
    • MySQL5.0及新版本,InnoDB能夠在服務器端過濾掉行就釋放鎖;但在早期版本,只有在事務提交後才能釋放鎖。
  • 若是不使用索引查找和鎖定行的話,MySQL可能會作全表掃描並鎖住全部的行,而無論是否須要。
  • InnoDB在二級索引上使用共享(讀)鎖,但訪問主鍵索引須要排他(寫)鎖,這消除了使用覆蓋索引的可能性(不理解?????),而且使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非鎖定查詢要慢得多。

3.4 索引案例學習

設計一個在線約會網站,用戶信息表包括國家、地區、城市、性別、眼睛顏色等等。網站必須支持上面這些特徵的各類組合來搜索用戶,還必須容許根據用戶的最後在線時間、其餘會員對用戶的評分等對用戶進行排序並對結果進行限制。

  • 使用索引排序,仍是先檢索數據再排序?使用索引排序會嚴格限制索引和查詢的設計。

3.4.1 支持多種過濾條件

  • 先看哪些列擁有不一樣的取值,哪些列在WHERE子句中出現得最頻繁

    • country和sex選擇性一般比較低,考慮到使用頻率,建議將(sex, country)做爲查詢前綴

      • 即便查詢沒有使用sex列,也可在查詢條件中新增AND SEX IN('m', 'f')來繞過。但若是列有太多的值而致使IN()列表太長,或則IN()的數量太多致使有太多的組合,則不建議使用該技巧。
      • 基本原則之一:考慮表上的全部選項。當設計索引時,不要只爲現有的查詢考慮須要哪些索引,還須要考慮對查詢進行優化。若是發現某些查詢須要建立新索引,可是這個查詢又會下降另外一些查詢的效率,那麼應該考慮優化原有的查詢,在優化查詢和索引找到最佳的平衡,而不是一味追求最完美索引。
  • 考慮其餘常見的WHERE組合列表,並須要瞭解哪些組合在沒有合適索引的狀況下會很慢。
    • (sex, country, age)、(sex, country, region, age)(sex, country, region, city, age)都很常見
      • 這會須要大量的索引。若是想盡可能重用索引,可使用前面提到的IN()技巧
      • 若是沒有指定這個字段搜索,就須要定義一個所有國家列表,或者國家的所有地區列表,來確保索引前綴有一樣的約束(組合全部國家、地區、性別將會是一個很是大的條件)
  • 爲一些生僻的搜索條件(好比has_pictures,eye_color,eduaction)來設計索引
    • 這些列選擇性高,使用也不頻繁,能夠選擇忽略,讓MySQL多掃描一些額外的行
    • 或者在age列的前面加上這些列,在查詢是使用IN()技巧來處理搜索時沒有這些列的場景。
  • 爲何要將age列放在最後?age列有什麼特殊的地方?
    • 儘量讓MySQL使用更多的索引列,由於查詢只能使用索引的最左前綴,直到遇到第一個範圍條件。前面的列都是等於條件,age列則大可能是範圍條件。
    • 雖然能夠用IN()來代替範圍查詢,例如age IN(18, 19, 20),但不是全部的範圍查詢均可以轉換。
    • 基本原則之二:儘量將須要作範圍查詢的列放到索引後面,以便優化器能使用盡量多的索引列。

3.4.2 避免多個範圍條件

假設有一個last_online列並但願經過下面的查詢顯示在過去幾週上線過的用戶:

WHERE eye_color IN('brown', 'blue', 'hazel')
    AND hair_color IN('black', 'red', 'blonde', 'brown')
    AND sex IN("M", "F")
    AND last_online > DATE_SUB(NOW(), INTERVAL 7 DAY)
    AND age BETWEEN 18 AND 25;

-- MySQL會將age>18和age IN(18,19)都認爲是範圍查詢(經過EXPLAIN查看),但兩種訪問效率是不一樣的,由於第二個查詢是多個等值條件查詢。對MySQL來講,沒法在使用範圍查詢後面的其餘索引列,但對多個等值範圍查詢沒有這個限制。複製代碼
  • 這個查詢有兩個範圍條件,MySQL沒法同時使用它們。
  • 若是沒法將age字段轉換爲一個IN()的列表,而且要求對這兩個維度的範圍查詢的速度很快,很遺憾沒有一個直接的辦法解決該問題,但能夠將其中的一個範圍查詢轉換成一個簡單的等值比較:
    • 事先計算好一個active列,這個字段由定時任務來維護。當用戶每次登錄時,將對應值設置爲1,而且將過去連續7天未登錄的用戶的值設置爲0
    • 這個方法可使用(active, sex, country, age)索引。active並非徹底精確的,由於對這類查詢的精度要求並不高。若是須要精確次數,能夠把last_online列放到WHERE子句,但不加入到索引中。因此這個查詢條件無法使用任何索引,但由於這個條件的過濾性不高,即便在索引中加入該列也沒有太大的幫助,或者說缺少合適的索引對該查詢的影響也不明顯。
  • 若是用戶系統同時看到活躍和不活躍用戶,能夠在查詢中使用IN()列表。另外一個可選方案是爲不一樣的組合建立單獨的索引,至少要包含(active, sex, country, age),(active, country, age),(sex, country, age)和(country, age),這些索引對某個具體的查詢來講多是更優化的,可是考慮到索引的維護額額外的空間佔有代價,並非一個好策略。

3.4.3 優化排序

  • 對選擇性很是低的列,能夠增長一些特殊的索引來作排序。例如,能夠建立(sex, rating),這個查詢同時使用了ORDER BY和LIMIT,若是沒有索引會很慢

    SELECT <cols> FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10

  • 即便有索引,若是用戶界面上要翻頁,而且翻頁翻到比較靠後時查詢也可能很是慢:

    SELECT <cols> FROM profiles WHERE sex="M" ORDER BY rating LIMIT 10000, 10;

    不管如何建立索引,這種查詢都是嚴重的問題。由於隨着偏移量的增長,MySQL須要花費大量的時間來掃描須要丟棄的數據。反範式化、預先計算和緩存多是解決這類查詢的僅有策略。一個更好的辦法是限制用戶可以翻頁的數量,而實際上這對用戶體驗的影響並不大,由於用戶不多真正在意搜索結果的第10000頁。

  • 優化這類索引另外一個較好的辦法是使用延遲關聯,經過使用覆蓋索引查詢返回須要的主鍵,再根據這些主鍵關聯原表得到須要的行。這能夠減小MySQL掃描那些須要丟棄的行數。

    SELECT <cols> FROM profiles INNER JOIN (
      SELECT <primary key cols> FROM profiles 
      WHERE sex="M" ORDER BY rating LIMIT 10000, 10
    ) AS x USING(<primary key cols>);複製代碼

3.5 維護和索引表

維護表的三個目的:找到並修復損壞的表,維護準確的索引統計信息,減小碎片

3.5.1 找到並修復順壞的表

損壞的索引會致使查詢返回錯誤的結果或莫須有的主鍵衝突問題,嚴重時還會致使數據庫崩潰。

  • 嘗試運行CHECK TABLE來檢查是否發生了表損壞(注意有些引擎不支持該命令),一般可以找出大多數的表和索引的錯誤。
  • 修復表錯誤的辦法:
    • 可使用REPAIR TABLE來修復損壞的表(注意有些引擎不支持該命令)。
    • 若是存儲引擎不支持REPAIR TABLE,也可經過一個不作任何操做的ALTER來重建表,如修改表的存儲引擎爲當前引擎:ALTER TABLE innodb_dbl ENGINE=INNODB;
    • 將數據導出一份,而後再從新導入。
    • 使用第三方工具
    • 若是損壞的是系統區域,或者是表的"行數據"區域,而不是索引,那麼以前的辦法就沒有用了。只能從備份中恢復表,或者嘗試從損壞的數據文件中儘量恢復數據。
  • 若是InnoDB引擎的表出現了損壞,那麼必定是發現了嚴重的錯誤,須要馬上調查下緣由。由於InnoDB的設計通常不會出現損壞。若是發生損壞,多是數據庫的硬件問題,或者在MySQL外部操做了數據文件,亦或是InnoDB的缺陷(不太可能)。不存在任何查詢讓InnoDB損壞。
    • 若是出現了數據損壞,最重要的是找出緣由,而不是簡單的修復,不然頗有可能會不斷的損壞。能夠經過設置innodb_force_recovery參數進入InnoDB的強制恢復數據模式來修復數據。

3.5.2 更新索引統計信息

  • MySQL的查詢優化器經過兩個API來了解存儲引擎的索引值分佈信息:
    • records_in_range(),經過傳入兩個邊界值獲取在這個範圍大概有多少條記錄。對某些存儲引擎如MyISAM返回精確值,對InnoDB返回一個估算值。
    • info(),返回各類類型的數據,包括索引的基數(每一個鍵值有多少條記錄)
  • 若是存儲引擎向優化器提供的掃描行數信息是不許確的數據,或者執行計劃自己太複雜而沒法精確地獲取各個階段匹配的行數,那麼優化器會使用索引統計信息來估算掃描行數。
  • MySQL優化器使用的是基於成本的模型,而衡量成本的主要指標就是一個查詢須要掃描多少行。若是表沒有統計信息,或者統計信息不許確,優化器極可能作出錯誤的決定。經過運行ANALYZE TABLE來從新生成統計信息解決這個問題。而每種存儲引擎實現的統計信息的方式不一樣,須要進行ANALYZE TABLE的頻率和每次運行的成本也不一樣:
    • Memory引擎根本不存儲索引統計信息
    • MyISAM將索引統計信息存儲在磁盤中,ANALYZE TABLE須要進行一次全索引掃描來計算索引基數,在整個過程當中須要鎖表。
    • 直到MySQL5.5版本,InnoDB也不在磁盤存儲索引統計信息,而是經過隨機的索引訪問進行評估並將其存儲在內存中。
  • 使用SHOW INDEX FROM table;命令來查看索引的基數(cadinality)。基數顯示了存儲引擎估算索引列有多少個不一樣的取值。在MySQL5.0及之後的版本,能夠經過INFORMATION_SCHEMA.STATISTICS表很方便地查詢到這些信息,不過若是服務器的庫表很是多,從這裏獲取元數據的速度會很是慢,並且會給MySQL帶來額外的壓力。
  • InnoDB的統計信息:
    • 該引擎經過抽樣的方式來計算統計信息,首先隨機地讀取少許的索引頁面,而後以此爲樣本計算索引的統計信息。老版本中樣本頁數是8,新版本能夠設置innodb_stats_sample_pages來設置樣本頁的數量。理論上越大的值能夠幫助生成更準確的索引信息,特別是對某些超大數據表來講。
    • 會在表首次打開,或者執行ANALYZE TABLE,抑或表的大小發生很是大的變化(該變化超過十六分之一或者新插入20億行)的時候計算索引的統計信息。
    • 會在打開某些INFORMATION_SCHEMA表,或者使用SHOW TABLE STATUS和SHOW INDEX,抑或MySQL客戶端開啓自動補全功能的時候都會觸發索引統計信息的更新。
      • 若是服務器上有大量的數據,可能會致使嚴重的問題,尤爲是IO比較慢的時候,客戶端或者監控程序觸發索引信息採樣更新時可能會致使大量的鎖,並給服務器帶來額外的壓力。能夠關閉innodb_stats_on_metadata參數來避免上面提到的問題。

3.5.3 減小索引和數據的碎片

  • 索引碎片化:
    • B-Tree索引可能會碎片化,這會下降查詢的效率。碎片化的索引可能會以不好或者無序的方式存儲在磁盤上。
    • 根據設計,B-Tree須要隨機磁盤訪問才能定位到葉子頁,因此隨機訪問是不可避免的。然而,若是葉子頁在物理分佈上是順序且緊密的,那麼查詢的性能就會更好。不然對於範圍查詢、索引覆蓋掃描等操做來講,速度可能會下降不少倍;對於索引覆蓋掃描這一點更明顯
  • 表的數據存儲碎片化(比索引碎片化更加複雜):
    • 類型:
      • 行碎片(Row fragmentation):數據行被存儲爲多個地方的多個片斷中。及時查詢只從索引中訪問一行記錄,也會致使性能降低。
      • 行間碎片(Intra-row fragmentation):指邏輯上順序的頁,或者行在磁盤上不是順序存儲的。行間碎片對諸如全表掃描和聚簇索引掃描之類的操做有很大的影響,由於這些操做本來可以從磁盤上的順序存儲的數據收益。
      • 剩餘空間碎片(Free space fragmentation):數據頁中有大量的空餘空間。這會致使服務器讀取大量不須要的數據,形成浪費。
    • 對MyISAM表,這三類碎片化均可能發生;但InnoDB不會出現短小的行碎片,它會移動短小的行並重寫到一個片斷中。
  • 從新整理數據方式:
    • OPTIMIZE TABLE
    • 導出再導入
    • 排序算法重建索引(針對MyISAM)
    • 「在線」添加和刪除索引的功能,能夠經過先刪除,而後在從新建立索引來消除索引碎片(針對最新版本InnoDB)
    • 經過一個不作任何操做的ALTER TABLE <table> ENGINE = <engine>;來重建表(針對不支持OPTIMIZE TABLE的引擎)
  • 應該經過一些實際測量而不是隨意假設來肯定是否須要消除索引和表的碎片化,還要考慮數據是否已達到穩定狀態(若是進行碎片整理將數據壓縮到一塊兒,可能會致使後續的更新操做觸發一系列的頁分裂和重組,對性能形成不良的影響直到數據達到新的穩定狀態)

3.6總結

  • MySQL和存儲引擎訪問數據的方式,加上索引的特性,使得索引成爲一個影響數據訪問的有利而靈活的工做(不管數據實在磁盤仍是在內存中)
  • 大多數狀況下都會使用B-Tree索引,其餘類型的索引大多隻適用於特殊目的。
  • 選擇索引和編寫利用這些索引的查詢時,有以下三個原則始終須要記住:
    • 單行訪問是很慢的。若是服務器從存儲中讀取一個數據塊只是爲了獲取其中的一行,那麼就浪費了不少工做。最好的讀取的塊中能包含儘量多須要的行。使用索引能夠建立位置引用提升效率。
    • 按順序訪問範圍數據是很快的。有兩個緣由:順序IO不須要屢次磁盤尋道而比隨機IO快不少;若是服務器可以按須要的順序讀取數據,就再也不須要額外的排序操做,而且GROUP BY查詢也無須再作排序和將行按組進行聚合計算。
    • 索引覆蓋查詢是很快的。若是一個索引包含了查詢須要的全部列,那麼存儲引擎就不須要再回表查找行,避免了大量的單行訪問。
  • 編寫查詢語句應該儘量選擇合適的索引以免單行查找,儘量地使用數據原生順序而避免額外的數據排序操做,並儘量使用索引覆蓋查詢。
  • 對某些查詢不可能建立一個「三星」索引,必需要有所取捨,或者尋求替代策略(例如反範式話、或者提早計算彙總表)
  • 理解索引的工做原理來建立最適合的索引
  • 判斷爲一個系統建立的索引的合理性:按響應時間對查詢進行分析。
    • 找出那些消耗時間最長的或給服務器帶來最大壓力的查詢
    • 檢查這些查詢的schema,SQL和索引結構
    • 判斷是否有查詢掃描了太多的行,是否作了不少額外的排序或者使用了臨時表,是否使用隨機IO訪問數據,或者是有太多回表查詢那些不在索引中的列的操做。
  • 若是一個查詢沒法從全部可能的索引中獲益,則應該看看是否能夠建立一個更合適的索引來提高性能。若是不行,也要嘗試是否能夠重寫該查詢,將其轉化成一個可以高效利用現有索引或者新建立索引的查詢。
相關文章
相關標籤/搜索