深入MySQL索引,這篇千萬不能錯過

Ki1tFJ

大家好,我是【碼老思】,索引是一個數據庫繞不開的話題,今天和大家一起聊聊。

1. 索引

索引是對數據庫表中一列或多列的值進行排序的一種結構。 MySQL索引的建立對於MySQL的高效運行是很重要的,索引可以大大提高MySQL的檢索速度。索引只是提高效率的一個因素,如果你的MySQL有大數據量的表,就需要花時間研究建立最優秀的索引,或優化查詢語句。

簡單類比一下,數據庫如同書籍,索引如同書籍目錄,假如我們需要從書籍查找與 xx 相關的內容,我們可以直接從目錄中查找,定位到 xx 內容所在頁面,如果目錄中沒有 xx 相關字符或者沒有設置目錄(索引),那隻能逐字逐頁閱讀文本查找,效率可想而知。

1.1 索引優缺點

優點

以常見的B+樹索引爲例,按照順序存儲數據。總結來看有如下三個優點:

  1. 索引大大減少了服務器需要掃描的數據量。
  2. 索引可以幫助服務器避免排序和建立臨時表。
  3. 索引可以將Order By等隨機IO轉爲順序IO。

缺點:

  1. 創建索引和維護索引(發生數據的增、刪、改時)要耗費時間,這種時間隨着數據量的增加而增加
  2. 索引需要佔物理空間,除了數據表佔用數據空間之外,每一個索引還要佔用一定的物理空間,如果需要建立聚簇索引,那麼需要佔用的空間會更大
  3. 對於非常小的表,大部分情況下簡單的全表掃描更高效;

1.2 索引的原理

MySQL中的數據是保存在磁盤上的,引入索引的目的就是提高訪問磁盤上數據的效率。

那麼這裏先簡單介紹一下磁盤IO和預讀,磁盤讀取數據靠的是機械運動,每次讀取數據花費的時間可以分爲尋道時間、旋轉延遲、傳輸時間三個部分。

  • 尋道時間指的是磁臂移動到指定磁道所需要的時間,主流磁盤一般在5ms以下;
  • 旋轉延遲就是硬盤通過盤片的旋轉,使得要讀取的扇區轉到讀寫頭的下方所需要花費的時間的平均值,比如一個磁盤7200轉,表示每分鐘能轉7200次,也就是說1秒鐘能轉120次,旋轉延遲就是1/120 / 2 = 4.17ms;
  • 傳輸時間指的是從磁盤讀出或將數據寫入磁盤的時間,一般在零點幾毫秒,相對於前兩個時間可以忽略不計。

那麼訪問一次磁盤的時間,即一次磁盤IO的時間約等於5+4.17 = 9ms左右,聽起來還挺不錯的,但要知道一臺500 -MIPS的機器每秒可以執行5億條指令,因爲指令依靠的是電的性質,換句話說執行一次IO的時間可以執行40萬條指令,數據庫動輒十萬百萬乃至千萬級數據,每次9毫秒的時間,顯然是個災難。考慮到磁盤IO是非常高昂的操作,計算機操作系統做了一些優化,當一次IO時,不光把當前磁盤地址的數據,而是把相鄰的數據也都讀取到內存緩衝區內,因爲局部預讀性原理告訴我們,當計算機訪問一個地址的數據的時候,與其相鄰的數據也會很快被訪問到。每一次IO讀取的數據我們稱之爲一頁(page)。具體一頁有多大數據跟操作系統有關,一般爲4k或8k,也就是我們讀取一頁內的數據時候,實際上才發生了一次IO,這個理論對於索引的數據結構設計非常有幫助。

如果有一種數據結構,每次查找數據時把磁盤IO次數控制在一個很小的數量級,最好是常數數量級,這樣就能大大提高數據的訪問效率,在這種背景下,索引應運而生。

在數據庫中,索引是分很多種類的。而不同的種類很顯然是爲了應付不同的場合,這裏介紹幾種常見的索引底層數據結構。

Hash表

哈希表是做數據快速檢索的有效利器。哈希算法也叫散列算法,就是把任意值(key)通過哈希函數變換爲固定長度的 key 地址,然後通過這個地址來獲取具體數據。
oI0gBo
但是哈希算法有個數據碰撞的問題,也就是哈希函數可能對不同的 key 會計算出同一個結果,解決碰撞問題的一個常見處理方式就是鏈地址法或者當鏈表過長時轉爲紅黑樹(JDK1.8的HashMap的處理方法)。

哈希算法只需要計算一次就可以獲取到對應的數據,檢索速度非常快。但是 Mysql 並沒有採取哈希作爲其底層算法,因爲哈希算法沒辦法做數據高效範圍查找,因此不適合作爲MySQL的底層索引結構。

自適應Hash索引

自適應Hash索引(Adatptive Hash Index,內部簡稱AHI)是InnoDB的三大特性之一,還有兩個是 Buffer Pool簡稱BP、雙寫緩衝區 Doublewrite Buffer。

自適應Hash索引 = 自適應 + hash索引: 1、自適應即我們不需要自己處理,當InnoDB引擎根據查詢統計發現某一查詢滿足hash索引的數據結構特點,就會給其建立一個hash索引;2、hash索引底層的數據結構是散列表(Hash表),其數據特點就是比較適合在內存中使用,自適應Hash索引存在於InnoDB架構中的緩存中(不存在於磁盤架構中),見下面的InnoDB架構圖
B7YZjf

Innodb存儲引擎會監控對錶上二級索引的查找,如果發現某二級索引被頻繁訪問,二級索引成爲熱數據,建立哈希索引可以帶來速度的提升,則:
t9pZDc
經常訪問的二級索引數據會自動被生成到hash索引裏面去(最近連續被訪問三次的數據),自適應哈希索引通過緩衝池的B+樹構造而來,因此建立的速度很快。

InnoDB的自適應Hash索引是默認開啓的,可以通過配置參數設置:innodb_adaptive_hash_index = off進行關閉。

自適應Hash算法的也有明顯的缺點:

1、hash自適應索引會佔用innodb buffer pool;
2、自適應hash索引只適合搜索等值的查詢,如select * from table where index_col='xxx',而對於其他查找類型,如範圍查找,是不能使用的;
3、極端情況下,自適應hash索引纔有比較大的意義,可以降低邏輯讀。

二叉查找樹

二叉查找樹是一種支持數據快速查找的數據結構,如圖下所示:

x4a69v

二叉查找樹的時間複雜度是 O(logN),也可以實現範圍查找,但是普通的二叉查找樹有個致命缺點:極端情況下會退化爲線性鏈表,二分查找也會退化爲遍歷查找,時間複雜退化爲 O(N),檢索性能急劇下降。

在數據庫中,數據的自增是一個很常見的形式,比如一個表的主鍵是 id,而主鍵一般默認都是自增的,如果採取二叉樹這種數據結構作爲索引,那上面介紹到的不平衡狀態導致的線性查找的問題必然出現。因此,簡單的二叉查找樹存在不平衡導致的檢索性能降低的問題,是不能直接用於實現 Mysql 底層索引的。

紅黑樹

紅黑樹是一種自平衡二叉查找樹,通過在插入和刪除節點時進行顏色變換和旋轉操作,使得樹始終保持平衡狀態,它具有以下特點:

  1. 每個節點非紅即黑;
  2. 根節點總是黑色的;
  3. 每個葉子節點都是黑色的空節點(NIL 節點);
  4. 如果節點是紅色的,則它的子節點必須是黑色的(反之不一定);
  5. 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。

aVNEaT

紅黑樹並不追求嚴格的平衡,而是大致的平衡,一定程度上解決了二叉查找樹可能的平衡問題。但是紅黑樹的平衡性相對較弱,可能會導致樹的高度較高,也會導致一些數據需要進行多次磁盤 IO 操作才能查詢到,這也是 MySQL 沒有選擇紅黑樹的主要原因。(在數據庫中主鍵自增,主鍵一般都是數百萬數千萬的,這種情況下可能導致紅黑樹一直存在右傾的趨勢,長此以往嚴重影響查詢性能)

因爲紅黑樹在插入和刪除節點時只需進行 O(1) 次數的旋轉和變色操作,即可保持基本平衡狀態,TreeMap、TreeSet 以及 JDK1.8 的 HashMap 底層都用到了紅黑樹。

AVL樹

AVL 樹的特點是保證任何節點的左右子樹高度之差不超過 1,因此也被稱爲高度平衡二叉樹,它的查找、插入和刪除在平均和最壞情況下的時間複雜度都是 O(logN)。

KfOL91
AVL樹採用了旋轉操作來保持平衡,雖然解決了二叉查找樹可能存在的不平衡問題,但是需要頻繁地進行旋轉操作(複雜度O(logN))來保持平衡,因此會有較大的計算開銷進而降低了查詢性能。

而且在使用 AVL 樹時,每個樹節點僅存儲一個數據,而每次進行磁盤 IO 時只能讀取一個節點的數據,如果需要查詢的數據分佈在多個節點上,那麼就需要進行多次磁盤 IO。

磁盤 IO 有個有個特點,就是從磁盤讀取 1B 數據和 1KB 數據所消耗的時間是基本一樣的,我們就可以根據這個思路,我們可以在一個樹節點上儘可能多地存儲數據,一次磁盤 IO 就多加載點數據到內存,這就是 B 樹,B+樹的的設計原理了。

B樹

下面這個 B 樹,每個節點限制最多存儲兩個 key,一個節點如果超過兩個 key 就會自動分裂。比如下面這個存儲了 7 個數據 B 樹,只需要查詢兩個節點就可以知道 id=7 這數據的具體位置,也就是兩次磁盤 IO 就可以查詢到指定數據,優於 AVL 樹。

Fo1YNt

下面是一個存儲了 16 個數據的 B 樹,同樣每個節點最多存儲 2 個 key,查詢 id=16 這個數據需要查詢比較 4 個節點,也就是經過 4 次磁盤 IO。看起來查詢性能與 AVL 樹一樣。

nU2V8K

但是考慮到磁盤 IO 讀一個數據和讀 100 個數據消耗的時間基本一致,那我們的優化思路就可以改爲:儘可能在一次磁盤 IO 中多讀一點數據到內存。這個直接反映到樹的結構就是,每個節點能存儲的 key 可以適當增加。

當我們把單個節點限制的 key 個數設置爲 6 之後,一個存儲了 7 個數據的 B 樹,查詢 id=7 這個數據所要進行的磁盤 IO 爲 2 次。

HvimYg

一個存儲了 16 個數據的 B 樹,查詢 id=7 這個數據所要進行的磁盤 IO 爲 2 次。相對於 AVL 樹而言磁盤 IO 次數降低爲一半。

b7Uzdw
所以數據庫索引數據結構的選型而言,B 樹是一個很不錯的選擇。總結來說,B 樹用作數據庫索引有以下優點:

  • 優秀檢索速度,時間複雜度:B 樹的查找性能等於 O(h * logN),其中 h 爲樹高,n 爲每個節點關鍵詞的個數;
  • 儘可能少的磁盤 IO,加快了檢索速度;
  • 可以支持範圍查找。

B+樹

爲了進一步的樹的高度,在每一個節點內存儲更多的數據,引入B+樹來解決這個問題。

相比B樹,B+樹有幾個特點:

  • B 樹一個節點裏存的是數據,而 B+樹存儲的是索引(地址),所以 B+樹一個節點能存很多索引。
  • B+樹葉子節點會存放所有的數據,而所有葉子節點之間,用了一個鏈表串聯起來,便於基於範圍查找。

mj8nK0

通過 B 樹和 B+樹的對比我們看出,B+樹節點存儲的是索引,在單個節點存儲容量有限的情況下,單節點也能存儲大量索引,使得整個 B+樹高度降低,減少了磁盤 IO。其次,B+樹的葉子節點是真正數據存儲的地方,葉子節點用了鏈表連接起來,這個鏈表本身就是有序的,在數據範圍查找時,更具備效率。因此 Mysql 的索引用的就是 B+樹,B+樹在查找效率、範圍查找中都有着非常不錯的性能。

爲什麼選擇B+樹來實現索引?

總結來講,主要有以下4點原因:

  • B+ 樹的層級更少:相較於 B 樹 B+ 每個非葉子節點存儲的關鍵字數更多,樹的層級更少所以查詢數據更快
  • B+ 樹查詢速度更穩定:B+ 所有關鍵字數據地址都存在葉子節點上,所以每次查找的次數都相同所以查詢速度要比B樹更穩定;
  • B+ 樹天然具備排序功能:B+ 樹所有的葉子節點數據構成了一個有序鏈表,在查詢大小區間的數據時候更方便,數據緊密性很高,緩存的命中率也會比B樹高。
  • B+ 樹全節點遍歷更快:B+ 樹遍歷整棵樹只需要遍歷所有的葉子節點即可,,而不需要像 B 樹一樣需要對每一層進行遍歷,這有利於數據庫做全表掃描。

B+樹一個節點有多大?3層B+樹能存放多少數據?

InnoDB存儲引擎也有自己的最小儲存單元——頁(Page),一個頁的大小默認是16K。B+樹一個節點的大小設爲一頁或頁的倍數最爲合適。因爲如果一個節點的大小 < 1頁,那麼讀取這個節點的時候其實讀取的還是一頁,這樣就造成了資源的浪費。 在 MySQL 中 B+ 樹的一個節點大小爲“1頁”,也就是16K。之所以設置爲一頁,是因爲對於大部分業務,一頁就足夠了。

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+

接下來可以大概估計下3層B+樹能存放多少數據,首先InnoDB的B+樹中,非葉子節點存的是key + 指針;葉子節點存的是數據行。
對於葉子節點,如果一行數據大小爲1k,那麼一頁就能存16條數據;對於非葉子節點,如果key使用的是bigint,則爲8字節,指針在mysql中爲6字節,一共是14字節,則16k能存放 16 * 1024 / 14 = 1170 個索引指針。於是可以算出,對於一顆高度爲2的B+樹,根節點存儲索引指針節點,那麼它有1170個葉子節點存儲數據,每個葉子節點可以存儲16條數據,一共 1170 x 16 = 18720 條數據,具體可以參考下圖。

而對於高度爲3的B+樹,就可以存放 1170 x 1170 x 16 = 21902400 條數據(兩千多萬條數據),也就是對於兩千多萬條的數據,我們只需要高度爲3的B+樹就可以完成,通過主鍵查詢只需要3次IO操作就能查到對應數據。所以在 InnoDB 中B+樹高度一般爲3層時,就能滿足千萬級的數據存儲,所以一個節點爲1頁,也就是16k是比較合理的。

2Mx9xQ

MyISM和InnoDB中的索引實現

這兩個引擎底層數據和索引的組織方式並不一樣,MyISAM 引擎把數據和索引分開了,一人一個文件,這叫做非聚集索引方式;Innodb 引擎把數據和索引放在同一個文件裏了,這叫做聚集索引方式。從兩者建表之後生成的文件就可以看出來:

Innodb 創建表後生成的文件有:

  • frm:創建表的語句
  • idb:表裏面的數據+索引文件

MyISAM 創建表後生成的文件有:

  • frm:創建表的語句
  • MYD:表裏面的數據文件(myisam data)
  • MYI:表裏面的索引文件(myisam index)

1. MyISAM引擎的底層實現(非聚集索引方式)

MyISAM 用的是非聚集索引方式,即數據和索引落在不同的兩個文件上。MyISAM 在建表時以主鍵作爲 KEY 來建立主索引 B+樹,樹的葉子節點存的是對應數據的物理地址。我們拿到這個物理地址後,就可以到 MyISAM 數據文件中直接定位到具體的數據記錄了。

CzR0IH
當我們爲某個字段添加索引時,我們同樣會生成對應字段的索引樹,該字段的索引樹的葉子節點同樣是記錄了對應數據的物理地址,然後也是拿着這個物理地址去數據文件裏定位到具體的數據記錄。

2. InnoDB 引擎的底層實現(聚集索引方式)

InnoDB 是聚集索引方式,因此數據和索引都存儲在同一個文件裏。首先 InnoDB 會根據主鍵 ID 作爲 KEY 建立索引 B+樹,如左下圖所示,而 B+樹的葉子節點存儲的是主鍵 ID 對應的數據,比如在執行 select * from user_info where id=15 這個語句時,InnoDB 就會查詢這顆主鍵 ID 索引 B+樹,找到對應的 user_name='Bob'。

這是建表的時候 InnoDB 就會自動建立好主鍵 ID 索引樹,這也是爲什麼 Mysql 在建表時要求必須指定主鍵的原因。當我們爲表裏某個字段加索引時 InnoDB 會怎麼建立索引樹呢?比如我們要給 user_name 這個字段加索引,那麼 InnoDB 就會建立 user_name 索引 B+樹,節點裏存的是 user_name 這個 KEY,葉子節點存儲的數據的是主鍵 KEY。拿到主鍵 KEY 後,InnoDB 纔會去主鍵索引樹里根據剛在 user_name 索引樹找到的主鍵 KEY 查找到對應的數據。

OmDtY0

總結對比

InnoDB 和 MyISAM 相比而言,MyISAM 查詢性能更好。MyISAM 直接找到物理地址後就可以直接定位到數據記錄,但是 InnoDB 查詢到葉子節點後,還需要再查詢一次主鍵索引樹,纔可以定位到具體數據(針對非主鍵的查詢)。等於 MyISAM 一步就查到了數據,但是 InnoDB 要兩步,那當然 MyISAM 查詢性能更高。

InnoDB中的主鍵索引爲聚集索引,其他索引均爲非聚集索引。主要原因在於,一個表裏可能有很多個索引,InnoDB 都會給每個加了索引的字段生成索引樹,如果每個字段的索引樹都存儲了具體數據,那麼這個表的索引數據文件就會非常大,有很多數據冗餘。採用現有的設計方案,能夠在犧牲較少查詢性能的前提下,節省大量的磁盤空間。另外,如果和MyISAM一樣在主鍵索引和輔助索引的葉子節點中都存放數據指針,一旦數據發生遷移,則需要去重新組織維護所有的索引,這也會產生巨大的開銷。

1BM9IH

1.3 索引分類

按照數據結構維度劃分:

  • BTree 索引:MySQL 裏默認和最常用的索引類型。只有葉子節點存儲 value,非葉子節點只有指針和 key。存儲引擎 MyISAM 和 InnoDB 實現 BTree 索引都是使用 B+Tree,但二者實現方式不一樣(前面已經介紹了)。
  • 哈希索引:類似鍵值對的形式,一次即可定位。
  • RTree 索引:一般不會使用,僅支持 geometry 數據類型,優勢在於範圍查找,效率較低,通常使用搜索引擎如 ElasticSearch 代替。
  • 全文索引:對文本的內容進行分詞,進行搜索。目前只有 CHARVARCHAR ,TEXT 列上可以創建全文索引。一般不會使用,效率較低,通常使用搜索引擎如 ElasticSearch 代替。

按照物理存儲方式劃分:

  • 聚簇索引(聚集索引):索引結構和數據一起存放的索引,InnoDB 中的主鍵索引就屬於聚簇索引。
  • 非聚簇索引(非聚集索引):索引結構和數據分開存放的索引,二級索引(輔助索引)就屬於非聚簇索引。MySQL 的 MyISAM 引擎,不管主鍵還是非主鍵,使用的都是非聚簇索引。

按照應用維度劃分:

  • 主鍵索引:加速查詢 + 列值唯一(不可以有 NULL)+ 表中只有一個。
  • 普通索引:僅加速查詢。
  • 唯一索引:加速查詢 + 列值唯一(可以有 NULL)。
  • 覆蓋索引:一個索引包含(或者說覆蓋)所有需要查詢的字段的值。
  • 聯合索引:多列值組成一個索引,專門用於組合搜索,其效率大於索引合併。
  • 全文索引:對文本的內容進行分詞,進行搜索。目前只有 CHARVARCHAR ,TEXT 列上可以創建全文索引。一般不會使用,效率較低,通常使用搜索引擎如 ElasticSearch 代替。

聚集索引與非聚集索引

聚集索引和非聚集索引的根本區別是表記錄的排列順序和與索引的排列順序是否一致。聚集索引的葉節點就是數據節點。而非聚簇索引的葉節點仍然是索引節點,只不過有一個指針指向對應的數據塊

1. 聚簇索引(Clustered Index):聚集索引表記錄的排列順序和索引的排列順序一致(以InnoDB聚集索引的主鍵索引來說,葉子節點中存儲的就是行數據,行數據在物理儲器中的真實地址就是按照主鍵索引樹形成的順序進行排列的),所以查詢效率快,只要找到第一個索引值記錄,其餘就連續性的記錄在物理也一樣連續存放。

聚集索引對應的缺點就是修改慢,因爲爲了保證表中記錄的物理和索引順序一致,在記錄插入的時候,會對數據頁重新排序(因爲在真實物理存儲器的存儲順序只能有一種,而插入新數據必然會導致主鍵索引樹的變化,主鍵索引樹的順序發生了改變,葉子節點中存儲的行數據也要隨之進行改變,就會發生大量的數據移動操作,所以效率會慢)。因爲在物理內存中的順序只能有一種,所以聚集索引在一個表中只能有一個

2. 非聚簇索引(Clustered Index):非聚集索引制定了表中記錄的邏輯順序,但是記錄的物理和索引不一定一致(在邏輯上數據是按順序排存放的,但是物理上在真實的存儲器中是散列存放的),兩種索引都採用B+樹結構,非聚集索引的葉子層並不和實際數據頁相重疊,而採用葉子層包含一個指向表中的記錄在數據頁中的指針方式。

非聚集索引層次多,不會造成數據重排。所以如果表的讀操作遠遠多於寫操作,那麼就可以使用非聚集索引。但非聚集索引有一個很大的缺點就是可能產生二次查詢,也就是回表, 當查到索引對應的指針或主鍵後,可能還需要根據指針或主鍵再到數據文件或表中查詢。

主鍵索引

數據表的主鍵列使用的就是主鍵索引。一張數據表有隻能有一個主鍵,並且主鍵不能爲 null,不能重複。

在 MySQL 的 InnoDB 的表中,當沒有顯示的指定表的主鍵時,InnoDB 會自動先檢查表中是否有唯一索引且不允許存在 null 值的字段,如果有,則選擇該字段爲默認的主鍵,否則 InnoDB 將會自動創建一個 6Byte 的自增主鍵。

二級索引(輔助索引)

二級索引(Secondary Index)又稱爲輔助索引,是因爲二級索引的葉子節點存儲的數據是主鍵。也就是說,通過二級索引,可以定位主鍵的位置。

唯一索引,普通索引,前綴索引等索引屬於二級索引。

  1. 唯一索引(Unique Key):唯一索引也是一種約束。唯一索引的屬性列不能出現重複的數據,但是允許數據爲 NULL,一張表允許創建多個唯一索引。 建立唯一索引的目的大部分時候都是爲了該屬性列的數據的唯一性,而不是爲了查詢效率。
  2. 普通索引(Index):普通索引的唯一作用就是爲了快速查詢數據,一張表允許創建多個普通索引,並允許數據重複和 NULL。
  3. 前綴索引(Prefix):前綴索引只適用於字符串類型的數據。前綴索引是對文本的前幾個字符創建索引,相比普通索引建立的數據更小, 因爲只取前幾個字符。
  4. 全文索引(Full Text):全文索引主要是爲了檢索大文本數據中的關鍵字的信息,是目前搜索引擎數據庫使用的一種技術。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之後 InnoDB 也支持了全文索引。

覆蓋索引和聯合索引

覆蓋索引: 如果一個索引包含 (或者說覆蓋)所有需要查詢的字段的值,我們就稱之爲 覆蓋索引(Covering Index) 。我們知道在 InnoDB 存儲引擎中,如果不是主鍵索引,葉子節點存儲的是主鍵+列值。最終還是要“回表”,也就是要通過主鍵再查找一次,這樣就會比較慢。而覆蓋索引直接從輔助索引中就能得到查詢的記錄,而不需要查詢聚集索引中的記錄。

這裏舉個例子,我們有一張user表,Id列上有主鍵索引,同時在Name列上建立了輔助索引,

kFbxzq

如上圖,如果通過name進行數據檢索:

select * from users where name = ?

需要需要在name索引中找到name對應的Id,然後通過獲取的Id在主鍵索引中查到對應的行。整個過程需要掃描兩次索引,一次name,一次id。如果我們查詢只想查詢id的值,就可以改寫SQL爲:

select id from users where name = ?

因爲只需要id的值,通過name查詢的時候,掃描完name索引,我們就能夠獲得id的值了,所以就不需要再去掃面id索引,就會直接返回。當然,如果你同時需要獲取age的值:

select id,age from users where name = ?

這樣就無法使用到覆蓋索引了。

知道了覆蓋索引,就知道了爲什麼sql中要求儘量不要使用select *,要寫明具體要查詢的字段。其中一個原因就是在使用到覆蓋索引的情況下,不需要進入到數據區,數據就能直接返回,提升了查詢效率。在用不到覆蓋索引的情況下,也儘可能的不要使用select *,如果行數據量特別多的情況下,可以減少數據的網絡傳輸量。

聯合索引: 指對錶上的多個列進行索引,在使用聯合索引進行查詢時,會遵守下面提到的最左匹配原則。

爲什麼要使用聯合索引 ?

1. 減少開銷。建一個聯合索引 (col1,col2,col3),實際相當於建了 (col1)(col1,col2)(col1,col2,col3) 三個索引。每多一個索引,都會增加寫操作的開銷和磁盤空間的開銷。對於大量數據的表,使用聯合索引會大大的減少開銷!
2. 覆蓋索引。對聯合索引 (col1,col2,col3),如果有如下的 SQL:select col1,col2,col3 from test where col1=1 and col2=2;。那麼 MySQL 可以直接通過遍歷索引取得數據,而無需回表,這減少了很多的隨機 IO 操作。減少 IO 操作,特別的隨機 IO 其實是 DBA 主要的優化策略。所以,在真正的實際應用中,覆蓋索引是主要的提升性能的優化手段之一。
3. 效率高。索引列越多,通過索引篩選出的數據越少。有 1000W 條數據的表,有如下 SQL:select from table where col1=1 and col2=2 and col3=3,假設假設每個條件可以篩選出 10% 的數據,如果只有單值索引,那麼通過該索引能篩選出 1000W10%=100w 條數據,然後再回表從 100w 條數據中找到符合 col2=2 and col3=3 的數據,然後再排序,再分頁;如果是聯合索引,通過索引篩選出 1000w * 10% * 10% * 10% =1w,效率提升可想而知!

最左匹配原則:

最左前綴匹配原則指的是,在使用聯合索引時,MySQL 會根據聯合索引中的字段順序,從左到右依次到查詢條件中去匹配,如果查詢條件中存在與聯合索引中最左側字段相匹配的字段,則就會使用該字段過濾一批數據,直至聯合索引中全部字段匹配完成,或者在執行過程中遇到範圍查詢(如 >< )纔會停止匹配。對於 >=<=BETWEENlike 前綴匹配 的範圍查詢,並不會停止匹配。所以,我們在使用聯合索引時,可以將區分度高的字段放在最左邊,這也可以過濾更多數據。

例如:如果建立(a,b)順序的索引,我們的條件只有b=xxx,是匹配不到(a,b)索引的;但是如果查詢條件是a = 1 and b = 2或者b=2 and a=1就可以,因爲優化器會自動調整a,b的順序,並不需要嚴格按照索引的順序來;再比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)順序的索引,d是用不到索引的,因爲c字段是一個範圍查詢,它之後的字段會停止匹配。

爲什麼會形成最左匹配原則 ?

首先要知道,最左匹配原則都是針對聯合索引來說的,所以我們有必要了解一下聯合索引的原理。瞭解了聯合索引,那麼爲什麼會有最左匹配原則這種說法也就理解了。

我們都知道索引的底層是一顆B+樹,那麼聯合索引當然還是一顆B+樹,只不過聯合索引的健值數量不是一個,而是多個。構建一顆B+樹只能根據一個值來構建,因此數據庫依據聯合索引最左的字段來構建B+樹。
例子:假如創建一個(a,b)的聯合索引,那麼它的索引樹是這樣的:
6azAvJ

可以看到a的值是有順序的,1,1,2,2,3,3,而b的值是沒有順序的1,2,1,4,1,2。所以b = 2這種查詢條件沒有辦法利用索引,因爲聯合索引首先是按a排序的,b是無序的。同時我們還可以發現在a值相等的情況下,b值又是按順序排列的,但是這種順序是相對的。所以最左匹配原則遇上範圍查詢就會停止,剩下的字段都無法使用索引。例如a = 1 and b = 2 a,b字段都可以使用索引,因爲在a值確定的情況下b是相對有序的,而a>1and b=2,a字段可以匹配上索引,但b值不可以,因爲a的值是一個範圍,在這個範圍中b是無序的。

1.4 高性能索引策略

(1) 選擇合適的字段創建索引

應該創建索引的列

  • 在經常需要搜索的列上,可以加快搜索的速度,但創建索引時要避免添加不必要的字段。
  • 在作爲主鍵的列上,強制該列的唯一性和組織表中數據的排列結構
  • 在經常用在連接(JOIN)的列上,這些列主要是一外鍵,可以加快連接的速度
  • 在經常需要根據範圍(<,<=,=,>,>=,BETWEEN,IN)進行搜索的列上創建索引,因爲索引已經排序,其指定的範圍是連續的
  • 在經常需要排序(order by)的列上創建索引,因爲索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間;如果待排序的列有多個,可以在這些列上建立組合索引。
  • 在經常使用在WHERE子句中的列上面創建索引,加快條件的判斷速度。

不該創建索引的列

  • 對於那些在查詢中很少使用或者參考的列不應該創建索引。若列很少使用到,因此有索引或者無索引,並不能提高查詢速度。相反,由於增加了索引,反而降低了系統的維護速度和增大了空間需求。
  • 對於那些只有很少數據值或者重複值多的列也不應該增加索引。 這些列的取值很少,例如人事表的性別列,在查詢的結果中,結果集的數據行佔了表中數據行的很大比例,即需要在表中搜索的數據行的比例很大。增加索引,並不能明顯加快檢索速度。
  • 對於那些定義爲text, image和bit數據類型的列不應該增加索引。 這些列的數據量要麼相當大,要麼取值很少。

(2) 避免爲頻繁更新的字段創建索引

主要考慮索引的維護成本,雖然索引能帶來查詢上的效率,但是維護索引的成本也是不小的。 如果一個字段不被經常查詢,反而被經常修改,那麼就更不應該在這種字段上建立索引了。

(3) 索引不是越多越好

索引並不是越多越好,建議單張表索引不超過 5 個,索引可以提高效率同樣可以降低效率。索引可以增加查詢效率,但同樣也會降低插入和更新的效率,甚至有些情況下會降低查詢效率。

因爲 MySQL 優化器在選擇如何優化查詢時,會根據統一信息,對每一個可以用到的索引來進行評估,以生成出一個最好的執行計劃,如果同時有很多個索引都可以用於查詢,就會增加 MySQL 優化器生成執行計劃的時間,同樣會降低查詢性能。

(4) 考慮建立聯合索引

利用好最左匹配原則。在創建一個N列的聯合索引時,實際是創建了MySQL可利用的N個索引。多列索引可起幾個索引的作用,因爲可利用索引中最左邊的列集來匹配行。另外,如果是聯合索引,多個字段在一個索引上,那麼將會節約很大磁盤空間,且修改數據的操作效率也會提升。

在建立聯合索引的時候,注意避免索引的冗餘。比如在同一張表上建立兩個索引(a)和(a, b),能夠命中索引(a, b)就肯定能命中索引(a) ,那麼索引(a)就是冗餘索引。

(5) 字符串使用前綴索引

使用短索引。如果對字符串列進行索引,應該指定一個前綴長度,只要有可能就應該這樣做。例如,有一個CHAR(200)列,如果在前10個或20個字符內,多數值是唯一的,那麼就不要對整個列進行索引。對前10個或20個字符進行索引能夠節省大量索引空間,也可能會使查詢更快。較小的索引涉及的磁盤 IO 較少,較短的值比較起來更快。更爲重要的是,對於較短的鍵值,索引高速緩存中的塊能容納更多的鍵值,因此,MySQL 也可以在內存中容納更多的值。這樣就增加了找到行而不用讀取索引中較多塊的可能性。

假設這裏有一個user表,裏面有一個字段叫做email,如何給email字段添加索引呢?我們可以對比下兩種方式,

alter table user add index index1(email) 
alter table user add index index2(email(6))

上面兩種方式都是在添加索引,不同點就是第二種添加的僅僅是郵箱的前綴索引,因此在建立對應索引的B+樹時,下面這種前綴索引只會用到email的前6個字段。但是我們選擇的長度需要具有區分度,這個例子中,如果不同行數據的email字段前6個字符基本都一致,那樣的話前綴索引就沒有起到很好的效果,雖然節省了空間,但是查詢時間會變長。

那麼如何選擇合適的前綴長度呢? 建立索引之前,我們要關注前綴字段的區分度,區分度越大,性能越高,意味着重複的值就越少。這樣既可以節省空間,也可以不用增加更多的查詢成本。

(6) 避免索引失效

一般來講,索引失效的情況有如下10種情況:

ZVhJSs

  • 違反最左匹配法則:如果索引有多列,要遵守最左前綴法則,即查詢從索引的最左前列開始並且不跳過索引中的列。
  • 在索引列上進行操作:如計算、函數、(自動or手動)類型轉換等操作,會導致索引失效從而全表掃描。
  • 索引範圍條件右邊的列:索引範圍條件右邊的索引列會失效,例如聯合索引中,某個列使用了>或者<的範圍比較,後續的字段將不會使用索引。
  • 儘量使用覆蓋索引:SELECT * 不會直接導致索引失效,但它可能會帶來一些其他的性能問題比如造成網絡傳輸和數據處理的浪費、無法使用索引覆蓋;
  • 使用不等於:mysql在使用不等於(!=、<>)的時候無法使用索引會導致全表掃描(除覆蓋索引外);
  • like以通配符開頭:例如'%abc',相反如果使用'abc%'索引可以生效。
  • 字符串不加單引號導致索引失效。
  • 使用or連接:查詢條件中使用 or,且 or 的前後條件中有一個列沒有索引,涉及的索引都不會被使用到;
  • order by和group by的字段違法最左匹配法則或者非索引字段,會導致額外的文件排序(order by)和生成臨時表(group by),降低性能。

(7) 刪除未使用的索引

刪除長期未使用的索引,不用的索引的存在會造成不必要的性能損耗。MySQL 5.7 可以通過查詢 sys 庫的 schema_unused_indexes 視圖來查詢哪些索引從未被使用。

1.5 索引優化

1. 索引下推

索引下推(Index Condition Pushdown,簡稱ICP),是MySQL5.6版本的新特性,它能減少回表查詢次數,提高查詢效率。 從下面的架構圖可以看到,MySQL服務層負責SQL語法解析、生成執行計劃等,並調用存儲引擎完成數據的存儲和檢索。

ljqR74

索引下推就是將部分上層(服務層)負責的事情,交給了下層(引擎層)去處理。

沒有ICP時,MySQL的查詢可以分成如下幾步:

  • 存儲引擎讀取索引記錄;
  • 根據索引中的主鍵值,定位並讀取完整的行記錄;
  • 存儲引擎把記錄交給Server層去檢測該記錄是否滿足WHERE條件。

使用ICP的情況下,查詢過程:

  • 存儲引擎讀取索引記錄(不是完整的行記錄);
  • 判斷WHERE條件部分能否用索引中的列來做檢查,條件不滿足,則處理下一行索引記錄;
  • 條件滿足,使用索引中的主鍵去定位並讀取完整的行記錄(就是所謂的回表);
  • 存儲引擎把記錄交給Server層,Server層檢測該記錄是否滿足WHERE條件的其餘部分。

舉例子說明一下,假設我們需要從user表中根據name和age來查找用戶,你可能使用這樣的query,

select * from tuser where name like '張%' and age=10;
  • 在沒有使用ICP的情況下,在MySQL 5.6之前,存儲引擎根據通過聯合索引找到name like '張%' 的主鍵id(1、4),逐一進行回表掃描,去聚簇索引找到完整的行記錄,server層再對數據根據age=10進行篩選

x5qUsV

可以看到需要回表兩次,把我們聯合索引的另一個字段age浪費了。

  • MySQL 5.6 以後, 存儲引擎根據(name,age)聯合索引,找到name like '張%',由於聯合索引中包含age列,所以存儲引擎直接再聯合索引裏按照age=10過濾。按照過濾後的數據再一一進行回表掃描。

Y1O0eg
可以看到只回表了一次。

2. MRR優化

MRR,全稱「Multi-Range Read Optimization」,簡單來講,MRR 通過把「隨機磁盤讀」,轉化爲「順序磁盤讀」,從而提高了索引查詢的性能。

1. 沒有MRR優化的讀取效果

假設沒有MRR優化,當我們在執行下面的操作時,MySQL 會按照下圖的方式,去磁盤讀取數據(假設數據不在數據緩衝池裏):
ewdKdA

圖中紅色線就是整個的查詢過程,藍色線則是磁盤的運動路線。這張圖是按照 Myisam 的索引結構畫的,不過對於 Innodb 也同樣適用。

對於 Myisam,左邊就是字段 age 的二級索引,右邊是存儲完整行數據的地方。先到左邊的二級索引找,找到第一條符合條件的記錄(實際上每個節點是一個頁,一個頁可以有很多條記錄,這裏我們假設每個頁只有一條),接着到右邊去讀取這條數據的完整記錄。這樣會導致大量的離散讀操作,由於MySQL是以頁爲單位進行數據讀取的,如果這幾條數據不在相同的頁上,還會造成緩衝池中的頁不斷的被替換出去,嚴重影響讀取效率。MRR就是爲了減少這種離散讀的操作,在訪問磁盤獲取完整的數據之前,在緩衝區將需要訪問的行按照主鍵id進行排序,然後再去順序的讀取磁盤,將這種重複離散讀取的行爲降到最低。

2. 有MRR優化的讀取效果

有MRR之後,可以看到在讀取行數據之前,多了一個排序的過程。

DsDV4M

  • 對於 MyISAM,在去磁盤獲取完整數據之前,會先按照 rowid 排好序,再去順序的讀取磁盤。
  • 對於 Innodb,則會按照聚簇索引鍵值排好序,再順序的讀取聚簇索引。

總結來講,MRR的幾個優點:

1、磁盤和磁頭不再需要來回做機械運動,將隨機訪問轉化爲順序訪問。對於IO密集型的SQL查詢語句,能帶來巨大的性能提升。

2、可以充分利用磁盤預讀。

比如在客戶端請求一頁的數據時,可以把後面幾頁的數據也一起返回,放到數據緩衝池中,這樣如果下次剛好需要下一頁的數據,就不再需要到磁盤讀取。這樣做的理論依據是計算機科學中著名的局部性原理:當一個數據被用到時,其附近的數據也通常會馬上被使用。

3、在一次查詢中,每一頁的數據只會從磁盤讀取一次。


參考:


歡迎關注公衆號【碼老思】,獲取最通俗易懂的原創技術乾貨。

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