InnoDB存儲引擎支持事務,是一個通用的、平衡了高可用與高性能的存儲引擎。它的設計目標主要面向在線事務處理(OLTP)的應用。它的特點有行鎖設計、支持外鍵、支持類似Oracle的非鎖定讀等。
InnoDB存儲引擎支持以下幾種常見索引:B+樹索引、全文索引、哈希索引。我們平時在開發(fā)中使用最多的,就是B+樹索引。
B+樹索引是傳統(tǒng)意義上的索引,這是目前關系數(shù)據(jù)庫系統(tǒng)中最常用和最有效的索引。需要注意的是,B+樹索引并不能找到一個給定鍵值的具體行。B+樹索引能夠找到的只是被查找的數(shù)據(jù)行所在的頁。然后通過數(shù)據(jù)庫把頁讀入內(nèi)存,在內(nèi)存中進行查找,最后得到所需的數(shù)據(jù)。
B+樹索引是基于B+樹數(shù)據(jù)結構構建的,B+樹的數(shù)據(jù)結構在很多數(shù)據(jù)結構書中都有詳細描述,在此不再詳述。數(shù)據(jù)庫中的B+樹索引可以分為聚集索引和輔助索引,它們的內(nèi)部都是高度平衡的,葉子存放著所有的數(shù)據(jù)的樹結構。聚集索引與輔助索引不同的是,葉子節(jié)點存放的是一整行信息。
聚集索引(Cluster Index)
聚集索引就是按照每張表的主鍵構建一個B+樹,同時葉子節(jié)點存放的即為整張表的行記錄數(shù)據(jù),我們也將聚集索引的葉子節(jié)點稱為數(shù)據(jù)頁。聚集索引的結構決定了索引組織表中數(shù)據(jù)也是索引的一部分。由于B+樹索引本身是有序的,同時數(shù)據(jù)行也存儲在葉子節(jié)點上,因此通過聚集索引我們能夠很快的訪問針對范圍值的查詢。
注:
- 當你在表中創(chuàng)建了一個PRIMARY KEY時,InnoDB就將該索引作為了聚集索引,因此最好為任何一個你創(chuàng)建的表添加PRIMARY KEY。
- 如果一個表中沒有定義一個PRIMARY KEY,MySQL會把表中第一個非NULL且唯一的索引作為聚集索引。
- 如果表中沒有符合上述兩種情況的索引,InnnoDB就會隱式的在包含rowID的合成列上生成聚集索引。
(也即不管如何,InnoDB的表中一定會有一個聚集索引,不管是顯示還是隱式創(chuàng)建的)
由于實際的數(shù)據(jù)頁只能按照一顆B+樹進行排序,因此每張表只能擁有一個聚集索引。在多數(shù)情況下,查詢優(yōu)化器傾向于采用聚集索引,因為聚集索引能夠在B+樹葉子節(jié)點上直接找到數(shù)據(jù)。(聚集索引對主鍵的排序查找和范圍查找速度非常快。因為葉子節(jié)點就維護了數(shù)據(jù)行。)圖一展示了一個聚集索引的數(shù)據(jù)分布(所有數(shù)據(jù)行就直接在葉子節(jié)點上):
輔助索引(Secondary Index)
InnoDB中所有除聚集索引以外的所有索引都被稱為輔助索引。對于輔助索引,葉子節(jié)點并不包含行記錄的全部數(shù)據(jù)。葉子節(jié)點除了包含鍵值以外,每個葉子節(jié)點的索引行中還包含一個主鍵值。通過主鍵值InnoDB存儲引擎可以再次在聚集索引中找到對應索引項的行數(shù)據(jù)。輔助索引的存在不會影響數(shù)據(jù)在聚集索引中的組織,因此每張表上可以存在多個輔助索引。當通過輔助索引來查找數(shù)據(jù)時,InnoDB存儲引擎會遍歷輔助索引并獲得頁中的指向主鍵索引的主鍵,然后再通過主鍵索引依次找到一個完整的行記錄。因此,這就意味著通過輔助索引查找行,存儲引擎先找到輔助索引的葉子節(jié)點,得到對應的主鍵值,然后根據(jù)該主鍵值去聚集索引上查找對應的行數(shù)據(jù)。也即一次輔助索引查找需要兩次對B+樹進行查找。
輔助索引數(shù)據(jù)分布圖示:
B+樹索引的管理
索引的創(chuàng)建刪除通過兩種方法,一種是ALTER TABLE,一種是CREATE/DROP INDEX,具體語法如下:
CREATE INDEX | KEY idx_b ON t
ALTER TABLE t ADD INDEX | KEY idx_b (b(100));//前綴索引
ALTER TABLE t ADD INDEX | KEY idx_a_b (a,b);//聯(lián)合索引
ALTER TABLE t DROP INDEX | KEY idx_b;
DROP INDEX idx_b ON T;//刪除索引
通過SQL命令
SHOW INDEX FROM t;
可以查看表t上的索引情況,圖示如下:
該結果每行的含義如下:
列名 | 含義 |
---|---|
Table | 索引所在的表 |
Non_unique | 非唯一索引(PRIMARY KEY為0,因為他必須是唯一的) |
key_name | 索引名字,我們可以通過該名字來執(zhí)行DROP INDEX |
Seq_in_index | 索引中該列的位置 |
Column_name | 索引列的名字 |
Collation | 列以什么方式存在索引中。B+樹總是A,即排序的。若使用Heap存儲引擎,且建立了Hash索引,會顯示NULL,即非排序 |
Cardinality | 表示索引中唯一值的數(shù)目的估計值 |
Sub_part | 是否是列的部分被索引(即前綴索引),否則為NULL |
Packed | 關鍵字如何被壓縮,沒有則為NULL |
NULL | 索引列是否含有NULL值,YES表示該列允許為NULL |
Index_type | 索引類型。InnoDB存儲引擎只支持B+樹索引,這里都是顯示BTREE |
Comment | 注釋 |
在所有這些參數(shù)中,Cardinality的值十分關鍵,優(yōu)化器會根據(jù)該值來判斷是否使用該索引。但并非每次更新索引都會更新該值,這樣做的代價太大。如果我們需要主動去更新該值,可以使用ANALYZE TABLE命令。
這里,我們會多了解一下Cardinality值的意義:Cardinality值表示索引中不重復記錄的預估值。雖然該值是一個預估值,但是對于我們是否需要在某列上建立索引具有很重要的指導意義。有一個參數(shù)Cardinality/n_rows_in_tables應該盡可能的接近1,此時表明該列的值基本是相互唯一的,表明該列具有很高的選擇性,在該列上建立索引并通過該列進行條件查詢,可以極大的改善查詢速度,如果該值過于小,我們就需要考慮是否有必要在該列上建立索引。舉一個例子:
CREATE TABLE test(
id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '',
customer_id BIGINT(20) NOT NULL COMMENT '客戶ID',
staff_id BIGINT(20) NOT NULL COMMENT '職員ID',
sex TINYINT(1) DEFAULT 0 COMMENT '性別0-女 1-男',
company VARCHAR(50) NOT NULL COMMENT '公司名稱'.
PRIMARY KEY (id),
KEY idx_sex (sex),
KEY idx_cus_sta (customer_id,staff_id)
)ENGINE=InnoDB CHARSET=utf8;
如該表t所示,sex列只有兩種情況,可以想象在一般情況下通過SHOW INDEX查詢該表得到sex列的Cardinality的值一定是2,Cardinality/n_rows_in_tables的值一定非常小(接近于0)以sex為條件進行查詢時,每次也只能篩選一半的符合條件的數(shù)據(jù)記錄,該列的選擇性很低,一般沒有必要在這種列上建立索引。
在InnoDB存儲引擎中,Cardinality統(tǒng)計信息的更新發(fā)生在兩個操作中:INSERT和UPDATE。如果每次發(fā)生INSERT和UPDATE操作就進行Cardinality信息統(tǒng)計更新,會給數(shù)據(jù)庫帶來很大的負擔,因此InnoDB存儲引擎內(nèi)部對Cardinality值的更新策略是:
- 表中1/6數(shù)據(jù)發(fā)生了變化;
- stat_modified_counter>2000 000 000 ;這是為了應對這樣一種情況,表中某一行記錄頻繁的更新,但實際上表中的數(shù)據(jù)行數(shù)并沒有增加,故在InnoDB存儲引擎內(nèi)部有一個計數(shù)器stat_modified_counter,用來表示發(fā)生變化的次數(shù),大于2000 000 000同樣更新Cardinality的值。
InnoDB存儲引擎內(nèi)部對Cardinality值的統(tǒng)計和更新操作是通過采樣方法的。InnoDB存儲引擎默認對8個葉子節(jié)點進行采樣,過程如下:
- 取得B+樹索引中葉子節(jié)點的數(shù)目,記為N
- 隨機取得B+樹索引中的8個葉子節(jié)點,統(tǒng)計每個頁中不同記錄的個數(shù),記為M1,M2,M3...M8
- Cardinality=(M1+M2+...+M8)*N/8
很明顯我們可以看出,Cardinality值是通過對8個葉子節(jié)點預估而來。根據(jù)它的計算方法,我們可以知道Cardinality值是在不斷變化的。
B+樹索引的使用
在OLTP應用中,我們的查詢操作一次只是從數(shù)據(jù)庫中獲取一小部分數(shù)據(jù),如根據(jù)主鍵值查詢訂單信息,這就是一種典型的OLTP查詢。在這種情況下,通過該主鍵建立的B+樹索引能夠很快的獲取對應數(shù)據(jù)。在實際使用中,我們可能會用到以下一些索引。
- 前綴索引
對于Mysql中的字符列,我們可以選擇在某些列上建立前綴索引。因為有些時候,索引列很長,這會使得索引變得大且慢,通常這時候,我們可以索引該字符列開始的部分字符,這樣可以大幅節(jié)約索引空間,提高索引效率,但是需要注意的是,這樣也可能會降低索引的選擇性。因此,前綴索引的目標應該是:要選擇足夠長的前綴以保證高的選擇性,同時前綴不要太長(以節(jié)約空間)還是以test表為例,我們在company字符列上創(chuàng)建一個前綴索引:
ALTER TABLE test ADD INDEX idx_com company(20);//示例,前綴長度按實際情況選擇
具體在company字段上選擇多長的前綴呢?這個需要我們實際去試一試。
SELECT COUNT(DISTINCT LEFT(company,4))/COUNT(*) AS sel4,
COUNT(DISTINCT LEFT(company,5))/COUNT(*) AS sel5,
COUNT(DISTINCT LEFT(company,6))/COUNT(*) AS sel6,
......
FROM test;
通過對比sel4,sel5,sel6...的結果,就可以找到一個比較合適的前綴長度。
- 聯(lián)合索引
聯(lián)合索引是指在多個列上建立的索引。如下sql語句:
ALTER TABLE t ADD INDEX | KEY idx_a_b (a,b);//聯(lián)合索引
在B+樹中,所有鍵值都是排序的,而對于聯(lián)合索引,索引列的順序意味著索引首先按照最左列進行排序,其次是第二列,依次等等。因此,索引的順序非常重要。一個簡單的聯(lián)合索引的例子如下圖所示:
當不需要考慮排序和分組時,將選擇性最高的列放在索引的前面通常是更好的。這么做可以最快的過濾出所需要的行。以前面的test表為例,如果我們想要建立一個包含customer_id與staff_id的聯(lián)合索引,我們可以先比較一下兩個列的選擇性,可以執(zhí)行一下SQL語句:
SELECT COUNT(DISTINCT coustomer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity
FROM test;
比較customer_id_selectivity與staff_id_selectivity哪個值更接近1,也即對應列的選擇性就更高,該列就更適合放在聯(lián)合索引的前面。
- 覆蓋索引
InnoDB支持覆蓋索引(覆蓋索引并非是一種可以通過SQL語句創(chuàng)建的索引,與前綴索引、聯(lián)合索引不同,我們可以認為這是一種邏輯上的索引,即符合一定條件的索引我們都可以稱之為是覆蓋索引),即從輔助索引中就可以直接得到查詢的記錄,而不需要再次查詢聚集索引中的記錄。使用覆蓋索引的一個好處就是輔助索引不包含整行記錄,因此它的大小遠小于聚集索引,可以減少大量IO操作,查詢速度很快。
不是所有的索引都能夠成為覆蓋索引,覆蓋索引必須要存儲索引列的值。當我們發(fā)起一個一個被索引覆蓋的查詢時,在EXPLAIN的Extra列可以看到Using Index信息,表示該次查詢在索引上就直接獲得了所有需要的數(shù)據(jù),也即這是一個覆蓋索引。如下圖所示:
由于customer_id與staff_id同時存在于索引中,因此當查詢customer_id與staff_id時,就可以直接從輔助索引中可以獲取所需的字段,而不需要再次去查找聚集索引。
最后
InnoDB存儲引擎是我們平時使用最廣泛的MySQL存儲引擎。同時B+樹索引也是我們默認選擇的索引類型。正確的創(chuàng)建和使用索引是實現(xiàn)高性能查詢的基礎。而這些合理的索引操作是基于我們對InnoDB存儲引擎的內(nèi)部原理有一定的了解之上的。
本文只是關于InnoDB索引的簡單總結,如有問題,歡迎大家一起交流、討論。