1. 程式人生 > >MySQL InnoDB儲存引擎 聚集和非聚集索引

MySQL InnoDB儲存引擎 聚集和非聚集索引

B+樹索引

索引的目的在於提高查詢效率,可以類比字典,如果要查“mysql”這個單詞,我們肯定需要定位到m字母,然後從下往下找到y字母,再找到剩下的sql。如果沒有索引,那麼你可能需要把所有單詞看一遍才能找到你想要的,如果我想找到m開頭的單詞呢?或者ze開頭的單詞呢?是不是覺得如果沒有索引,這個事情根本無法完成?

我們都知道CPU是很快的,磁碟是很慢的,要想提高資料庫的訪問效率,可以說非常大的一個優化點就是減少磁碟IO訪問。每次查詢資料時把磁碟IO次數控制在一個很小的數量級,最好是常數數量級。那麼我們就想到如果一個高度可控的多路搜尋樹是否能滿足需求呢?就這樣,B+樹應運而生。B+樹索引的本質就是B+樹在資料庫中的實現,但是B+樹索引在資料庫中有一個特點是高扇出性,因此在資料庫中,B+樹的高度一般都在2-4層,這也就是說查詢某一鍵值得行記錄最多隻需要2-4次IO。這倒不錯,因為當前一般的機械磁碟每秒至少可以做100次IO,2-4次的IO意味著查詢時間只需要0.02-0.04秒。

資料庫中的B+樹索引可以分為聚集索引(clustered index)和輔助索引(secondary index),但是不管是聚集索引還是輔助的索引,其內部組都是B+樹的,即高度平衡的,葉子節點存放著所有的資料。聚集索引與輔助索引不同的是,葉子節點存放的是否是一整行的資訊。

聚集索引(聚簇索引)

InnoDB儲存引擎表是索引組織表,即表中資料按照主鍵順序存放。而聚簇索引就是按照每張表的主鍵構造一棵B+樹,同時葉子節點中存放的即為整張表的行記錄資料,也將聚集索引的葉子節點稱為資料頁。聚簇索引的這個特性決定了索引組織表中資料也是索引的一部分。同B+樹資料結構一樣,每個資料頁都通過一個雙向連結串列來進行連結。由於實際的資料頁只能按照一棵B+樹進行排序,因此每張表只能擁有一個聚集索引。在多數情況下,查詢優化器傾向於採用聚集索引。因為聚集索引能夠在B+樹索引的葉子節點上直接找到資料。此外,由於定義了資料的邏輯順序,聚集索引能夠特別快地訪問針對範圍值的查詢。查詢優化器能夠快速發現某一段範圍的資料頁需要掃描。

許多文件會告訴我們:聚集索引按照順序物理地儲存資料,但是試想一下,如果聚集索引必須按照特定順序存放物理記錄,則維護成本顯得非常之高。所以,聚集索引的儲存並不是物理上連續的,而是邏輯上連續的。這其中有兩點:一是前面說過的頁通過雙向連結串列連結,頁按照主鍵的順序排序;另一點是每個頁中的記錄也是通過雙向連結串列進行維護的,物理儲存上可以同樣不按照主鍵儲存。

聚集索引的另一個好處是,它對於主鍵的排序查詢和範圍查詢速度非常快。葉子節點的資料就是使用者所要查詢的資料。如使用者需要查詢一張註冊使用者的表,查詢最後註冊的10位使用者,由於B+樹索引是雙向連結串列的,使用者可以快速找到最後一個數據頁。並取出10條記錄。若用命令EXPLAIN進行分析,可得:

mysql> explain select * from info order by id limit 10\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: info
type: index
possible_keys: NULL
key: PRIMARY
key_len: 4
ref: NULL
rows: 10
Extra:
1 row in set (0.00 sec)

mysql> explain select * from info order by id limit 10\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: info
type: index
possible_keys: NULL
key: PRIMARY
key_len: 4
ref: NULL
rows: 10
Extra:
1 row in set (0.00 sec)

可以看到雖然使用ORDER BY對記錄進行排序,但是在實際過程中並沒有進行所謂的filesort操作,而這就是因為聚集索引的特點。

另一個是範圍查詢,即如果要查詢主鍵某一範圍內的資料,通過葉子節點的上層中間節點就可以得到頁的範圍,之後直接讀取資料頁即可。又如:

mysql> EXPLAIN select * from profile where id>10 and id<1000\G
id: 1
select_type: SIMPLE
table: profile
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 14868
Extra: Using where
1 row in set (0.00 sec)

mysql> EXPLAIN select * from profile where id>10 and id<1000\G
id: 1
select_type: SIMPLE
table: profile
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 14868
Extra: Using where
1 row in set (0.00 sec)

執行EXPLAIN得到了MySQL資料庫的執行計劃,並且在rows列中給出了一個查詢結果的預估返回行數。要注意的是,rows代表的是一個預估值,不是確切的值。

如下圖,是一顆B+樹(聚簇索引):
MySQL InnoDB聚簇及輔助索引

關於b+樹的定義可以參見B+樹,這裡只說一些重點,淺藍色的塊我們稱之為一個磁碟塊,可以看到每個磁碟塊包含幾個資料項(深藍色所示)和指標(黃色所示),如磁碟塊1包含資料項17和35,包含指標P1、P2、P3。P1表示小於17的磁碟塊,P2表示在17和35之間的磁碟塊,P3表示大於35的磁碟塊。真實的資料存在於葉子節點即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非葉子節點不儲存真實的資料,只儲存指引搜尋方向的資料項,如17、35並不真實存在於資料表中。

b+樹性質

通過上面的分析,我們知道IO次數取決於b+數的高度h。假設當前資料表的資料為N,每個磁碟塊的資料項的數量是m,則有h=㏒(m+1)N,當資料量N一定的情況下,m越大,h越小。而m = 磁碟塊的大小 / 資料項的大小,磁碟塊的大小也就是一個數據頁的大小,是固定的,如果資料項佔的空間越小,資料項的數量越多,樹的高度越低。這就是為什麼每個資料項,即索引欄位要儘量的小,比如int佔4位元組,要比bigint8位元組少一半。這也是為什麼b+樹要求把真實的資料放到葉子節點而不是內層節點,一旦放到內層節點,磁碟塊的資料項會大幅度下降,導致樹增高。當資料項等於1時將會退化成線性表。

當b+樹的資料項是複合的資料結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜尋樹的,比如當(張三,20,F)這樣的資料來檢索的時候,b+樹會優先比較name來確定下一步的搜尋方向,如果name相同再依次比較age和sex,最後得到檢索的資料。但當(20,F)這樣的沒有name的資料來的時候,b+樹就不知道下一步該查哪個節點,因為建立搜尋樹的時候name就是第一個比較因子,必須要先根據name來搜尋才能知道下一步去哪裡查詢。比如當(張三,F)這樣的資料來檢索時,b+樹可以用name來指定搜尋方向,但下一個欄位age的缺失,所以只能把名字等於張三的資料都找到,然後再匹配性別是F的資料了, 這個是非常重要的性質,即索引的最左匹配特性。

我們在查詢資料時,一般都會在經常被查詢的欄位上面建立一個索引(B+樹索引),這正是利用了索引中被排序的鍵值,通過內節點的索引功能及葉子節點中資料的有序性(索引預設是升序的),利用二分查詢極大的提高了查詢的效能,所以索引在資料庫中的作用是至關重要的。

b+樹的查詢過程

如圖所示,如果要查詢資料項29,那麼首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次IO,在記憶體中用二分查詢確定29在17和35之間,鎖定磁碟塊1的P2指標,記憶體時間因為非常短(相比磁碟的IO)可以忽略不計,通過磁碟塊1的P2指標的磁碟地址把磁碟塊3由磁碟載入到記憶體,發生第二次IO,29在26和30之間,鎖定磁碟塊3的P2指標,通過指標載入磁碟塊8到記憶體,發生第三次IO,同時記憶體中做二分查詢找到29,結束查詢,總計三次IO。真實的情況是,3層的b+樹可以表示上百萬的資料,如果上百萬的資料查詢只需要三次IO,效能提高將是巨大的,如果沒有索引,每個資料項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。

輔助索引

對於輔助索引(secondary index,也稱二級索引),葉子節點並不包含行記錄的全部資料,葉子節點除了包含鍵值以外,每個葉子節點中的索引行中還包含了一個書籤。該書籤用來告訴Innodb儲存引擎哪裡可以找到與索引相對應的行資料。由於Innodb儲存引擎是索引組織表,因此Innodb儲存引擎的輔助索引的書籤就是相應行資料的聚集索引鍵。

輔助索引的存在並不影響資料在聚集索引中的組織,因此每張表上可以有多個輔助索引。當通過輔助索引來尋找資料時,Innodb儲存引擎會遍歷輔助索引並通過葉級別的指標獲得指向主鍵索引的主鍵,然後再通過主鍵索引來找到一個完整的行記錄。舉例來說,如果在一棵高度為3的輔助索引樹種查詢資料,那需要對這棵輔助索引樹遍歷3次找到指定主鍵,如果聚集索引樹的高度同樣為3,那麼還需要對聚集索引樹進行3次查詢,最終找到一個完整的行資料所在的頁。因此一共需要6次邏輯IO訪問以得到最終的一個數據頁。

從上面的特性我們可以知道,一個表中,聚簇索引佔用的空間肯定是最大的,因為它是儲存了全部資料的,而輔助索引,是建立在某幾個需要經常查詢的列上面的,除了這幾個列之外,剩下的就是用來“回表”的指標資訊了,所以相對而言,輔助索引的佔用空間都會比聚簇索引小很多,特別是在一個表的列數很多或是這些列中包含大欄位的情況下,因為我們一般都不會在大欄位上直接建立索引。那這樣比較下來,在我們統計一個表總的精確行數時(查COUNT*),一些優化器就會選擇表中最小的索引來作為統計的目標索引,因為它佔用空間最小,IO也會最小,效能相應的更快一些。

上面說到了“回表”,所謂回表,就是在使用輔助索引時,因為輔助索引只儲存了部分資料,如果根據鍵值查詢到的資料不能包括全部目標資料時,就需要通過二級索引的指標,也就是鍵值對中的值,來找到聚簇索引的全部資料,然後根據完整的資料取出所需要的列的過程。這種在二級索引中不能找到所有需要的資料列的現象,被稱為非覆蓋索引,反之稱為覆蓋索引。因為回表本身是需要去另一個索引(聚簇索引)中查詢資料的,效能必然會受到影響,那為了儘可能的提高效能就需要儘量的減少回表次數,所以可以試著將出現頻率非常高的語句中所有使用到的列以合適的順序建一個二級索引,這樣所有需要的列都被這個二級索引覆蓋了,就不需要回表了,從而一定程度上提高了效能。這雖然是一個好的做法,但需要去權衡,因為需要考慮語句中涉及到的列數,這個語句出現的頻率及最終這個索引的大小。最壞的情況是建一個和聚簇索引差不多大的二級索引,這樣一方面是佔用空間比較大,另一方面是維護這個二級索引對這個表的整體修改效能也是有影響的,所以各方面都需要去權衡,然後再決定是不是要這樣做。

上面還說到了,在統計總行數的時候,可以直接使用二級索引來做,是因為有一個很明顯但很重要的前提:每個二級索引與聚簇索引的總行數是一樣的,並且一對一。只不過在每一個索引中,資料行的排序順序不同,可以想象二級索引行與聚簇索引行行之間都有虛線相連,並且二級索引中每一行都有且只有一條虛線指向聚簇索引中的一行資料,而聚簇索引的每一行,都會有相同個數的虛線指進來,這個數目就是二級索引的個數。至於二級索引與聚簇索引究竟是如何連起來的,我們後面會詳細講述。

輔助索引的指標

現在已經知道,聚簇索引儲存了所有資料,二級索引只儲存了部分資料,但二級索引是為了提高效能的,所以經常會被使用到,那如果二級索引中的資料不能滿足需求怎麼辦?這就用到了我們上面提到的“回表”,也就是二級索引中每行記錄中指標的作用。

關於聚簇索引及二級索引列之間的邏輯關係,我們分類如下:

自定義主鍵的聚簇索引:
索引結構:[主鍵列][TRXID][ROLLPTR][其它建表建立的非主鍵列]

參與記錄比較的列:主鍵列

內結點KEY列:[主鍵列]+PageNo指標

未定義主鍵的聚簇索引:
索引結構:[ROWID][TRXID][ROLLPTR][其它建表建立的非主鍵列]

參與記錄比較的列:只ROWID一列而已

內結點KEY列:[ROWID]+PageNo指標

自定義主鍵的二級唯一索引:
索引結構:[唯一索引列][主鍵列]

參與記錄比較的列:[唯一索引列][主鍵列]

內結點KEY列:[唯一索引列]+PageNo指標

自定義主鍵的二級非唯一索引:
索引結構:[非唯一索引列][主鍵列]

參與記錄比較的列:[非唯一索引列][主鍵列]

內結點KEY列:[非唯一索引列][主鍵列]+PageNo指標

未定義主鍵的二級唯一索引:
索引結構:[唯一索引列][ROWID]

參與記錄比較的列:[唯一索引列][ROWID]

內結點KEY列:[唯一索引列]+PageNo指標

未定義主鍵的二級非唯一索引:
索引結構:[非唯一索引列][ROWID]

參與記錄比較的列:[非唯一索引列][ROWID]

內結點KEY列:[非唯一索引列][ROWID]+PageNo指標

通過這六種情況,講清楚了聚簇索引記錄包含的列,二級索引記錄包括的列,以及在非葉子節點中分別包含的列,因為索引是用來檢索資料的,所以還講述了用來檢查記錄時,在二級索引及聚簇索引中,參與比較記錄大小的列分別是什麼,唯一索引與非唯一索引的區別等。

需要注意的一點是,上面講述的索引列的順序關係,與實際索引中記錄的物理儲存不是一回事,記錄的儲存格式是記錄的格式,而這個是索引在記憶體中是元組的組織關係,這個元組的順序體現的就是每個索引自己的邏輯順序,以什麼列建的索引,什麼列就會在最前面起到優先排序的作用。

我們這裡特別關注一下二級唯一索引的元組邏輯順序,二級唯一索引中,作為索引本身的索引列,就是我們上面所說的“鍵”,當這個元組需要回表時,在元組中儲存的聚簇索引列資訊,就是我們所說的“值”,這樣就形成了鍵值對。而對於二級非唯一索引而言,因為只有索引列本身再加上主鍵列才能保證索引記錄是唯一的,所以這二者合起來才能構成我們所說的“鍵”,而“值”就為空了,也就是說,二級非唯一索引中,在記錄構成方面,非葉結節點只是比葉子節點多了一個PageNo指標資訊。

從上面可以看到,二級索引元組中,首先儲存的就是每個索引定義的索引列,接著就是這條記錄對應的聚簇索引的主鍵列的值,而主鍵列是唯一的,所以二級索引回表時對應的記錄也是唯一的,這樣就形成了一種指標的效果。

不過有一點需要注意一下,二級索引回表時對應的聚簇索引,如果是使用者自定義的,有可能是自增列,也有可能是有邏輯意義的單列或者組合列的聚簇索引,如果使用者沒有自定義,則InnoDB會自動給聚簇索引分配一個主鍵列,不過是隱藏的列,即我們所熟知的Rowid列。基於此,如果是使用者自定義的聚簇索引,則二級索引指標指向的就是聚簇索引所包含的列,如果沒有自定義主鍵,那該指標就指向Rowid列了。