mysql索引最左匹配原理

原文地址

MySql索引

發佈於 2017-09-03  約 16 分鐘

索引(key)是存儲引擎用於快速找到記錄的一種數據結構。它和一本書中目錄的工作方式類似——當要查找一行記錄時,先在索引中快速找到行所在的位置信息,然後再直接獲取到那行記錄。
在MySql中,索引是在存儲引擎層而不是服務器層實現的,所以不同的存儲引擎對索引的實現和支持都不相同。

B-TREE索引

B-TREE索引是使用最多的索引。很多存儲引擎採用的都是B-TREE數據結構的變體實現該索引,例如InnoDB使用的是B+TREE,即每個葉子節點都包含指向下一個葉子節點的指針,從而方便葉子節點範圍遍歷。
不同存儲引擎使用B-TREE索引的方式也不同。例如MyISAM使用前綴壓縮技術使索引更小,而InnoDB則按照原數據格式進行存儲。再如MyISAM索引通過數據的物理位置引用被索引的行,而InnoDB則根據主鍵引用被索引的行。
B-TREE中的所有值都是按順序存儲的,每個葉子頁到根的距離相同。下圖展示了InnoDB中的B-TREE索引是如何工作的:
B-Tree數據結構

當查找一行記錄時,存儲引擎會先在索引中搜索。從索引的根節點開始,通過比較節點頁的值和要查找的值逐層進入下層節點,最底層葉子節點的指針指向的是被索引的數據。這樣的查找方式避免了全表掃描,加快訪問數據的速度。此外因爲B-Tree對索引列是順序存儲的,所以也很適合查找範圍數據。
下面是一個使用B-Tree索引的例子,有如下數據表:

CREATE TABLE People (
    last_name varchar(50) not null,
    first_name varchar(50) not null,
    dob date not null,
    gender enum('m','f') not null,
    key(last_name,first_name,dob)
)

這個建表語句在last_name、first_name、dob列上建立了一個聯合索引,下圖展示了該索引的存儲結構。
圖片描述

可以看到,聯合索引中的索引項會先根據第一個索引列進行排序,第一個索引列相同的情況下,會再按照第二個索引列進行排序,依次類推。根據這種存儲特點,B-Tree索引對如下類型的查找有效:

  1. 全值匹配:查找條件和索引中的所有列相匹配

  2. 匹配最左前綴:查找條件只有索引中的第一列

  3. 匹配列前綴:只匹配某一列值的開頭部分。這裏並不一定只能匹配第一個索引列的前綴。例如在確定第一個索引列的值時,也可以在第二個索引列上匹配列前綴。在上面例子中,對於查找姓爲Allen,名爲J開頭的人,也可以應用到索引。

  4. 匹配範圍值,或者精確匹配某一列並範圍匹配另外一列:例如查找姓在Allen和Barrymore之間的人,或者查找姓爲Allen,名字在某一個範圍內的人。

  5. 只訪問索引的查詢,即要查詢的值在索引中都包含,只需要訪問索引就行了,無需訪問數據行。這種索引被稱作覆蓋索引。

  6. 對於上面列出的查詢類型,索引除了可以用來查詢外,還可以用來排序。

下面是B-Tree索引的一些限制:

  1. 如果不是從索引的最左列開始查找,則無法使用索引。例如直接查找名字爲Bill的人,或查找某個生日的人都無法應用到上面的索引,因爲都跳過了索引的第一個列。此外查找姓以某個字母結尾的人,也無法使用到上面的索引。

  2. 不能在中間跳過索引中的某個列,例如不能查找姓爲Smith,生日爲某個特定日期的類。這樣的查詢只能使用到索引的第一列。

  3. 如果查詢中有某個列的範圍查詢,則該列右邊的所有列都無法使用索引優化查找。例如有查詢WHERE last_name='Smith' AND first_name LIKE 'J%' AND dob='1976-12-23',這個查詢只能使用到索引的前兩列,而不能使用整個索引。

通過上面列出的這些條件,可見對於一個B-TREE聯合索引,索引列的順序非常重要。

哈希索引

如果在列上建立哈希索引,則針對每一行數據,存儲引擎會根據所有的索引列計算出一個哈希碼,每個行計算出的哈希碼會組成一個哈希表,同時在哈希表中存儲了指向每個數據行的指針。只有精確匹配全部索引行的查詢條件才能利用到哈希索引。
例如有如下數據表,在表中fname列上建立了一個哈希索引:

CREATE TABLE testhash(
    fname VARCHAR(50) NOT NULL,
    lname VARCHAR(50) NOT NULL,
    KEY USING HASH(fname)
);

表中包含如下數據:
圖片描述

假設索引使用的哈希函數對fname字段的各個值生成如下哈希碼:
圖片描述

則索引中的哈希表結構如下:
圖片描述

對於下面的這條查詢:

select lname from testhash where fname = 'Peter';

MySql先計算Peter的哈希碼爲8784,根據這個哈希碼得到“指向第3行數據的指針”。然後獲取第三行數據,判斷第三行的fname是否爲Peter,以確保就是要查找的行(防止哈希衝突影響)。
因爲哈希索引中的哈希表只包含指向對應記錄行的指針,所以索引的結構十分緊湊,查找的速度也非常快。但也有下面這些限制:

  1. 哈希索引中不包含任何列的值,所以無法利用哈希索引避免讀取行。

  2. 哈希索引無法用於排序。

  3. 不支持部分索引列匹配查找。必須是使用全部索引列的查詢條件才能使用哈希索引優化查詢。

  4. 只能支持等值比較,不支持範圍查找。

  5. 哈希衝突會影響索引性能。當有哈希衝突時,必須遍歷每個哈希值相同索引項,找到對應的數據行與其進行比較,直到找到對應數據或遍歷結束。

InnoDB中有一個功能叫“自適應哈希索引”,當InnoDB注意到某些索引值使用的非常頻繁時,會在B-Tree索引之上再建立一層哈希索引,以加速查找效率。這是完全自動的內部行爲,用戶無法干預。
下面是藉助哈希算法優化索引的一個例子:
假設需要存儲大量的URL,並根據URL進行搜索查找。如果直接使用B-Tree索引存儲URL,因爲URL一般都比較長,存儲的內容就會很大。可以在表中新建一個列url_crc,這個列存儲的值爲crc32(url),並在url_crc列上建立一個B-Tree索引。這樣只需要很小的索引就可以爲超長的列建立索引,性能會提高很多。注意要使用下面形式的查詢語句進行查詢,因爲有可能會有哈希衝突,所以還要對url進行等值比較。

select id from url where url='https://segmentfault.com/write?draftId=1220000010978320' and
 url_crc=crc32('https://segmentfault.com/write?draftId=1220000010978320');

獨立的列

索引列不能是表達式的一部分,也不能是函數的參數,否則將無法使用索引進行查詢優化。列入下面兩個查詢都無法利用到索引:

SELECT actor_id FROM actor WHERE actor_id+1=5;
SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col)<=10;

雖然有些表達式很簡單,但MySql也無法解析方程式。應該養成簡化where條件的習慣,始終將索引列單獨放在比較符號的一側。

前綴索引

如果在很長的字符串列上建立索引,會導致索引變得大且慢。這時可以考慮只根據列的開始部分字符建立索引,這樣可以大大節約索引空間,從而提高索引效率。
對於BLOB、TEXT或很長的VARCHAR類型的列,必須使用前綴索引,因爲MySql不允許索引這些列的完整長度。
在使用前綴索引時,要注意保證索引的選擇性。索引的選擇性是指不重複的索引值和數據表記錄總數的比值。索引的選擇性越高則查詢效率越高,因爲選擇性高的索引在查找時會過濾掉更多的行。所以建立前綴索引時,選取的字符串前綴長度不能太短。

多列索引

像下面這樣在每個列上都建立一個單獨的索引是非常錯誤的:

CREATE TABLE t(
    t1 INT,
    t2 INT,
    t3 INT,
    KEY(t1),
    KEY(t2),
    KEY(t3)
)

當出現對多個索引列做相交(AND)操作的查詢時,代表需要一個包含所有相關列的聯合索引,而不是多個獨立的單列索引。
在MySql官方提供的示例數據庫sakila中,表film_actor在字段film_id和actor_id上各有一個單列索引,對於下面這條查詢語句,這兩個單列索引都不是很好的選擇:

SELECT film_id,actor_id FROM film_actor WHERE actor_id=1 OR film_id=1;

在老的MySql版本中,這個查詢會使用全表掃描。但在MySql5.0之後,查詢能夠同時使用這兩個單列索引進行掃描,然後將結果合併,相當於轉換成下面這條查詢:

SELECT film_id,actor_id FROM film_actor WHERE actor_id=1 
UNION
SELECT film_id,actor_id FROM film_actor WHERE film_id=1;

在MySql5.7中,執行上面查詢的執行計劃如下圖所示:
圖片描述

從執行計劃的type字段可以看到,MySql同時使用了兩個索引,並將各自的查詢結果合併。並且Extra字段描述了使用索引的詳細信息。
雖然MySql在背後對查詢進行了優化,使其可以同時利用兩個單列索引。但是這需要耗費大量的CPU和內存資源,所以直接將查詢改寫成UNION的方式會更好。像這種兩個列上都有索引的情況,用union代替or會得到更好的效果(注意要求兩個列上都建有索引,如果沒有索引,用union代替or反而會降低效率)。
如果在EXPLAIN中看到有索引合併,那就應該好好檢查一下查詢和表的結構,看看是不是已經是最優的。

聯合索引列的順序

將選擇性最高的列放到索引的最前列雖然是一條重要的參考準則,但通常不如避免隨機IO和排序那麼重要。所以在設計索引時,還要考慮到WHERE子句中的排序、分組和範圍條件等其他因素,這些因素可能對查詢的性能造成非常大的影響。

聚簇索引

因爲是存儲引擎負責實現索引,所以對於不同的存儲引擎,聚簇索引的實現方式有所區別。這裏討論的是InnoDB下的聚簇索引。
所謂聚簇索引,就是表中的數據行實際上是存放在聚簇索引的葉子頁中。因爲表中的數據行只有一份,所以一個表只能有一個聚簇索引。在InnoDB中,唯一的聚簇索引就是主鍵索引。如果沒有定義主鍵,InnoDB會選擇一個非空索引代替。如果連一個非空索引都沒有,則InnoDB會隱式定義一個主鍵來作爲聚簇索引。也就是說,對於InnoDB存儲引擎,它肯定是有且僅有一個聚簇索引的存在,並且InnoDB存儲引擎的表結構就是通過聚簇索引組織的。
下面定義了一個表結構:

CREATE TABLE layout_test (
    col1 int NOT NULL,
    col2 int NOT NULL,
    PRIMARY KEY(col1),
    KEY(col2)
); 

對於這個表結構,在InnoDB中建立的聚簇索引結構如下圖所示:
圖片描述

首先聚簇索引跟之前介紹過的索引一樣,採用的都是B-Tree的數據結構。不同的是,聚簇索引在葉子節點中,不僅保存了索引列的值,還保存了TID(事務ID)、RP(回滾指針)和表中所有非主鍵的列的值(這個例子中只有一個非主鍵列col2)。非葉子節點中還是隻包含索引列的值(就是主鍵)和指向下一個節點的指針。由此可以看到,在InnoDB中,整個表結構是用聚簇索引來維護的。
對於這裏的二級索引(非主鍵列,這裏是col2上的索引),它的葉子節點並不保存指向數據行的指針,而是保存了行的主鍵值。這樣當出現數據行的物理位置改變時,無需更新二級索引中的這個“指針”。
如果使用的是InnoDB存儲引擎,則應該保證主鍵是按照數據行的插入順序遞增的。可以定義一個代理鍵作爲主鍵,這種主鍵的數據可以和應用無關,最簡單的方式是使用AUTO_INCREMENT自增列。因爲如果主鍵的值是順序增長的,那麼每次insert操作其實就是簡單的把本次插入的記錄放到上一條記錄的後面,當達到一個頁的最大填充因子時(InnoDB默認的最大填充因子是頁大小的15/16,留出的部分空間用於以後修改),本次要插入的記錄就會寫到新的頁中。這種順序寫入的方式操作簡單,並且不會導致頁分裂,會有很高的效率。
相反,如果主鍵是一個隨機的值,InnoDB就無法簡單的總是把新行插入到索引的最後,而是要爲新的行尋找合適的位置(通常是已有數據的中間位置),這會導致分配空間、頁分裂等很多額外的工作。

覆蓋索引

如果一個索引包含所有需要查詢的字段,就稱之爲“覆蓋索引”。由於在索引的葉子節點中已經包含了要查詢的全部數據,所以就可以從索引中直接獲取查詢結果,而沒必要再回表查詢。
索引一般遠遠小於數據行的大小,如果只需要訪問索引,就會極大減少數據訪問量。而且索引是按照順序存儲,所以在進行範圍查詢時會比隨機從磁盤讀取每一條數據的I/O要少的多。由此看出,覆蓋索引能夠極大的提高查詢性能。
sakila數據庫中包含了由store_id和film_id組成的一個聯合索引,如下圖所示:
圖片描述

如果只查詢store_id和film_id這兩列,就可以使用這個索引做覆蓋索引。
圖片描述

EXPLAIN的Extra列如果是Using index,則代表這個查詢使用到了覆蓋索引。注意type字段和是否爲覆蓋索引毫無關係。

利用索引做排序

只有當索引的列順序和ORDER BY子句的順序完全一致,並且所有列的排序方向都一樣時,才能使用索引對結果進行排序。一般情況ORDER BY子句和查找型查詢的限制是一樣的,都需要滿足索引的最左前綴要求。但有一種特殊情況,如果在WHERE子句或JOIN子句中對索引前導列指定了常量,則order by子句可以不滿足索引的最左前綴的要求。
在表rental中,有一個在列(rental_date,inventory_id,customer_id)上建立的聯合索引rental_date,下圖描述了rental表的結構:
圖片描述

可以利用rental_date索引爲下面的查詢做排序:
圖片描述

雖然這裏的order by子句不滿足索引的最左前綴的要求,但由於在WHERE子句中指定了索引rental_date的第一列爲一個常量,所以仍然可以用於排序。
下面這個查詢也可以利用rental_date索引進行排序,因爲ORDER BY子句中使用的兩列就是索引的最左前綴。

. . .WHERE rental_date>'2005-05-25' ORDER BY rental_date,inventory_id;

下面的查詢就無法使用索引進行排序:
圖片描述

雖然在ORDER BY子句中滿足了最左索引前綴的要求,但由於查詢使用了兩種不同的排序方向,所以仍然無法使用索引進行排序。注意EXPLAIN中的type字段只是說明了這個SQL利用到了索引進行查詢,Extra字段才描述了有沒有使用到索引進行排序。
下面的查詢語句中ORDER BY子句中的兩個列都是逆序的,但仍然可以使用索引進行排序。所以是否能利用到索引進行排序與DESC和ASC無關,只要每個列的排序方向都相同,就可以利用到索引進行排序。

. . .WHERE rental_date='2005-05-25' 
ORDER BY inventory_id DESC,customer_id DESC
閱讀 4.6k 發佈於 2017-09-03

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