一、索引是什么
索引,在MySQL中也叫“鍵(key)”,是存儲引擎用於快速找到記錄的一種數據結構。如果把數據庫的一張表比作一本書,那索引則是這本書的目錄,通過目錄,我們能快速找到我們想要的主題所對應的頁碼。索引的作用即類似於書的目錄,幫助我們快速定位到相關數據行的位置。
好的索引能使查詢的性能提高幾個數量級,而差的索引在大數據量的表中甚至會使性能急劇下降。“最優”的索引有時比一個“好的”索引性能要好兩個數量級。
二、索引有哪些類型
索引有很多種類型,可以為不同的場景提供更好的性能。在MySQL中,索引是在存儲引擎層而非服務器層實現的,而不同的存儲引擎的索引的工作方式並不一樣,且不是所有的存儲引擎都支持所有類型的索引。同時,值得一提的是,不同的存儲引擎對同一類型的索引,其底層的實現一般是不同的。
MySQL支持以下幾種類型的索引。
(1)B-Tree索引
(2)哈希索引
(3)空間數據索引(R-Tree)
(4)全文索引
(5)其他索引類別
下面我將一一展開進行介紹。
三、 B-Tree索引
(一)B-Tree索引只是一個統稱術語
B-Tree索引是最常見的索引類型,它使用B-Tree數據結構來存儲數據,大多數MySQL引擎都支持這種索引。(Archive引擎是一個例外:5.1之前Archive不支持任何索引,直到5.1才開始支持單個自增列AUTO_INCREMENT的索引。)
在MySQL中,“B-Tree”只是一個術語的統稱,因為不同的存儲引擎可能使用的是其他存儲結構來實現這種索引,但僅僅只是命名為“B-Tree”。例如,NDB集群存儲引擎內部實際上使用了T-Tree結構存儲這種索引;InnoDB則使用的是B+Tree結構存儲這種索引。只是它們都將其命名為“B-Tree”。
(二)B-Tree索引在不同引擎中的差異
不同的存儲引擎使用B-Tree索引的方式也不同,性能也各有不同,各有優劣。下面拿MyISAM 和InnoDB進行對比。
表3-1 MyISAM和InnoDB中B-Tree的相關差異
|
MyISAM |
InnoDB |
存儲方式 |
前綴壓縮技術 |
按照原數據格式 |
引用方式 |
通過數據的物理位置引用被索引的行 |
根據主鍵引用被索引的行 |
(三)InnoDB的B-Tree技術實現是B+Tree
上面我們也提到,InnoDB的B-Tree索引從技術上來說實際上是B+Tree實現的,這種實現使得所有的值都是按照順序存儲的(所以很適合查找范圍數據),並且每一個葉子頁到根的距離相同。MyISAM使用的結構有所不同,但基本思想類似。
圖3.1 建立在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) );
索引對多個值進行排序的依據是CREATE TABLE語句中定義索引時列的順序。
(五)B-Tree索引支持的查詢類型
MySQL的B-Tree索引適用於全鍵值、鍵值范圍或鍵前綴查找,其中鍵前綴查找只適用於根據最左前綴的查找。前面所述的索引可細分為如下幾種類型。
(1)全值匹配
全值匹配指的是和索引中的所有列進行匹配。
例如上面的People表的索引(last_name,first_name,dob)可以用於查找last_name=’Zeng’,first_name=’Chuang’,dob=’1996-01-01’的人。這就是使用了索引中的所有列進行匹配,即全值匹配。
(2)匹配最左前綴
可以只使用索引的第一個列進行匹配。
例如可以用於查找last_name=’Zeng’的人,即用於查找姓為Zeng的人,這里只使用了索引的最左列進行匹配,即匹配最左前綴。
(3)匹配列前綴
可以只匹配某一列的值的開頭部分。
例如可以用於查找last_name LIKE ‘Z%’的人,即用於查找所有以Z開頭的姓的人,這里只使用了索引最左列的前綴進行匹配,即匹配列前綴。
(4)匹配范圍值
可以只適用索引的第一列查找符合某個范圍內的數據。
例如可以用於查找last_name BETWEEN ‘Qiu’ AND ‘Zeng’的人,即用於查找姓在Qiu和Zeng之間的人,這里只使用了索引最左列的前綴進行范圍匹配,即匹配范圍值。
(5)精確匹配某一列並范圍匹配另外一列
可以使第一列全匹配,第二列范圍匹配。
例如可以用於查找last_name=’Zeng’ AND first_name LIKE ’C%’的人,即用於查找姓是Zeng,名字以C開頭的人,這里使用了索引的最左列精確匹配,第二列進行范圍匹配,即精確匹配某一列並范圍匹配另外一列。
(6)只訪問索引的查詢
查詢只需訪問索引,而無須訪問數據行。
例如select last_name, first_name where last_name=’Zeng’; 這里只查詢索引所包含的last_name和first_name列,則無須讀取數據行。
(六)B-Tree索引的限制
根據上面介紹的B-Tree索引支持的查詢類型,我們可以知道,它同樣會存在一些限制。
(1)只能按照索引的最左列開始查找。
例如People表中的索引無法用於查找first_name為’Chuang’的人,也無法查找某個特定生日的人,因為這兩個列都不是最左數據列。
(2)只能按照索引最左列的最左前綴進行匹配。
例如People表中的索引無法查找last_name LIKE ‘%eng’的人,雖然last_name就是此索引的最左列,但MySQL索引無法查找以‘eng’結尾的last_name的記錄。
(3)只能按照索引定義的順序從左到右進行匹配,不能跳過索引中的列。
例如People表中的索引無法用於查找last_name=’Zeng’ AND bod=’1996-01-01’的人,因為MySQL無法跳過索引中的某一列而使用索引中最左列和排在末尾的列進行組合。如果不指定索引中中間的列,則MySQL只能使用索引的最左列,即第一列。
(4)如果查詢中有某個列的范圍查詢,則其右邊所有列都無法使用索引優化查找。
例如有這樣一個查詢:where last_name=’Zeng’ AND first_name LIKE ’C%’ AND dob=’1996-01-01’; 這個查詢只能使用索引的前兩列,因為這里LIKE是一個范圍條件,則first_name后面的索引列都將失效。(優化點:盡量不要在索引列中使用LIKE等范圍條件,改用多個等於條件來替代,保證后面的索引列能生效。)
閱讀到這里,我們應該明白了索引列的順序是多么的重要,上面的這些限制都和索引列的順序有關。在性能優化時,可能需要使用相同的列但順序不同的索引來滿足不同類型的查詢需求。
四、哈希索引
(一)哈希索引是什么
哈希索引(hash index)基於哈希表實現,只有精確匹配索引所有列的查詢才有效。
對於每一行數據,存儲引擎都會對所有的索引列計算一個哈希碼(hash code),不同鍵值的行計算出來的哈希碼也不一樣。哈希索引將所有的哈希碼存儲在索引中,並在哈希表中保存指向每個數據行的指針。
圖4.1 hash索引圖解
(二)不同存儲引擎對哈希索引的支持
表4-1 各存儲引擎對哈希索引的支持情況概覽
|
Memory引擎 |
NDB集群引擎 |
InnoDB引擎 |
是否支持哈希索引 |
顯示支持,是Memory引擎表的默認索引類型(也支持B-Tree索引) |
支持唯一哈希索引,所起作用特殊 |
自適應哈希索引(adaptive hash index) |
(1) Memory引擎的哈希索引
Memory引擎不僅支持唯一哈希索引,還支持非唯一哈希索引。非唯一哈希索引指的是:如果多個列的哈希值相同,索引會以鏈表的方式存放多個指向不同記錄的指針到同一個哈希條目中。
圖4.2 鏈表方式存放哈希索引
使用Memory引擎在建表時創建哈希索引
CREATE TABLE testhash ( fname VARCHAR(50) NOT NULL, Lname VARCHAR(50) NOT NULL, KEY USING HASH(fname) ) ENGINE=MEMORY;
(2) NDB集群引擎的哈希索引
后續將深入閱讀官方文檔,理解NDB集群引擎中哈希索引的作用:https://dev.mysql.com/doc/mysql-cluster-excerpt/5.7/en/mysql-cluster-ndbd-definition.html
(3) InnoDB引擎的哈希索引
InnoDB引擎有一個特殊的功能叫“自適應哈希索引(adaptive hash index)”:當InnoDB注意到某些索引值被使用得非常頻繁時,它會在內存中基於B-Tree索引之上再創建一個哈希索引,讓B-Tree索引也具有哈希索引的一些優點,如快速的哈希查找。
“自適應哈希索引”是一個完全自動的、內部的行為,用戶無法控制或配置,不過如果有必要,完全可以關閉該功能。
(三)哈希索引的優勢、限制及適用場景
(1)優勢:哈希索引查找的速度非常快。
原因:索引自身只需存儲對應的哈希值,使得索引的結構十分緊湊。
(2)哈希索引的限制
1)不能使用索引中的值來避免讀取行。因為哈希索引只包含哈希值和行指針,而不存儲字段值。
2)不能用於排序。因為哈希索引數據不是按照索引值順序存儲的。
3)不支持部分索引列匹配查找。因為哈希索引必須使用索引列的全部內容來計算哈希值。
4)只支持等值比較查詢(包括=、IN()、< = >),不支持任何范圍查詢(如where price > 100)。
5)當出現哈希沖突時(不同的索引列值卻有相同的哈希值),訪問速度會變慢。因為存儲引擎必須遍歷鏈表中所有的行指針,逐行進行比較,知道找到所有符合條件的行。
6)若哈希沖突很多,一些索引維護操作的代價也會很高。如:在某個哈希沖突很多的列上建立哈希索引,當從表中刪除一行時,存儲引擎需要遍歷對應哈希值的鏈表中的每一行,找到並刪除對應行的引用。沖突越多,代價越大。
(3)哈希索引的適用場景
因為上面的這些限制,使得哈希索引適用的場景比較有限。而一旦適用哈希索引,則它帶來的性能提升將非常顯著。
如,在數據倉庫應用中有一種經典的“星型”schema,需要許多關聯才能建立查找表,哈希索引就非常適合查找表的需求。
(四)創建自定義哈希索引
(1)思路
在B-Tree基礎上創建一個偽哈希索引。這和真正的哈希索引不是一回事,因為還是使用B-Tree進行查找,但是它使用哈希值而不是鍵本身進行索引查找。只需要在查詢的WHERE字句中手動指定使用哈希函數。
(2)哈希索引查找實例
一張表中存儲了大量的URL,並需要根據URL進行搜索查找。如果使用B-Tree來存儲URL,存儲的內容會很大。正常情況下會有如下查詢:
SELECT id FROM url WHERE url=”http://www.mysql.com”;
若刪除原來URL列上的索引,而新增一個被索引的url_crc列,使用CRC32做哈希,就可以使用下面的方式查詢:
SELECT id FROM url WHERE url=”http://www.mysql.com” AND url_crc=CRC32(“http://www.mysql.com”);
這樣做性能會非常高,因為MySQL優化器會使用這個選擇性很高而體積很小的基於url_crc列的索引來完成查找。及時有多個記錄相同的索引值,查找仍然很快,因為MySQL優化器會先篩選出匹配的索引行記錄,然后根據具體的url值進行比對,返回完全符合條件的行。
(3)使用觸發器維護哈希值
新增一列url_crc列之后需要維護這個哈希值。可以手動維護,也可以使用觸發器實現。
首先創建如下表:
CREATE TABLE pseudohash ( id int unsigned NOT NULL auto_increment, url varchar(255) NOT NULL, url_crc int unsigned NOT NULL DEFAULT 0, PRIMARY KEY(id) );
然后創建觸發器:
DELIMITER // CREATE TRIGGER pseudohash_crc_ins BEFORE INSERT ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url); END; // CREATE TRIGGER pseudohash_crc_upd BEFORE UPDATE ON pseudohash FOR EACH ROW BEGIN SET NEW.url_crc=crc32(NEW.url); END; // DELIMITER ;
最后驗證觸發器如何維護哈希索引:
INSERT INTO pseudohash(url) VALUES(‘http://www.mysql.com’); SELECT * FROM pseudohash; UPDATE pseudohash SET url=‘http://www.mysql.com’ WHERE id=1; SELECT * FROM pseudohash;
(4)規避使用SHA1()和MD5()
SHA1()和MD5()計算出來的哈希值是非常長的字符串,會浪費大量空間,比較時也會更慢。SAH1()和MD5()是強加密函數,設計目標是最大限度消除沖突,但這里並不需要這樣高的要求。
簡單哈希函數的沖突在一個可以接受的范圍,同時又能夠提供更好的性能。
(5)處理哈希沖突
1)自定義哈希函數
如果表的數據量非常大,CRC32()會出現大量的哈希沖突,則可以考慮自己實現一個簡單的64位哈希函數。這個自定義函數要返回整數,而不是字符串。一個簡單的辦法可以使用MD5()返回值的一部分來作為自定義哈希函數。這可能比自己寫一個哈希算法的性能要查,但這樣實現最簡單:
SELECT CONV(RIGHT(MD5(‘http://www.mysql.com’),16),16,10) AS HASH64;
注:CONV(N,from_base,to_base)
N是要轉換的數據,from_base是原進制,to_base是目標進制。
2)WHERE字句中包含常量值
當使用哈希索引進行查詢的時候,必須在WHERE字句中包含常量值:
SELECT id FROM url WHERE url=“http://www.mysql.com” AND url_crc=CRC32(“http://www.mysql.com”);
因為所謂的“生日悖論”,出現哈希沖突的概率的增長速度可能比想象的要快得多。CRC32()返回的是32位的整數,當索引有93000條記錄時出現沖突的概率是1%。
如果不想查詢具體值,例如只是統計記錄數(不精確的),則可以不帶入列值,直接使用CRC32()的哈希值查詢即可。
還可以使用如FNV64()函數作為哈希函數,這是移植自Percona Server的函數,可以以插件的方式在任何MySQL版本中使用,哈希值為64位,速度快,且沖突比CRC32()要少很多。
五、空間數據索引(R-Tree)
MyISAM表支持空間索引,可以用作地理數據存儲。和B-Tree索引不同,這類索引無須前綴索引。空間索引會從所有維度來索引數據。查詢時,可以有效地使用任意維度來組合查詢。必須使用MySQL的GIS相關函數如MBRCONTAINS()等來維護數據。MySQL的GIS支持並不完善,所以大部分人都不會使用這個特性。
開源關系數據庫系統中對GIS的解決方案做得比較好的是PostgreSQL和PostGIS。
六、全文索引
全文索引是一種特殊類型的索引,它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文搜索和其他幾類索引的匹配方式完全不一樣。它有許多需要注意的細節,如停用詞、詞干和復數、布爾搜索等。全文索引更類似於搜索引擎做的事情,而不是簡單的WHERE條件匹配。
七、其他索引類型
TokuDB使用形樹索引(fractal tree index),這是一類較新開發的數據結構,既有B-Tree的很多優點,也避免了B-Tree的一些缺點。
還有InnoDB的聚簇索引、覆蓋索引等。
ScaleDB使用Patricia tries。
其他存儲引擎技術如InfiniDB和Infobright則使用了一些特殊的數據結構來優化某些特殊的查詢。
參考文獻
[1]Baron Scbwartz, Peter Zaitsev, Vadim Tkacbenko. 高性能MySQL[M].第三版.北京:電子工業出版社, 2013:141-146