索引的定義
MySQL官方對索引的定義爲:索引(Index)是協助MySQL高效獲取數據的數據結構。
本質上,索引的目的是爲了提升查詢效率,經過不斷地縮小想要獲取數據的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,咱們能夠老是用同一種查找方式來鎖定數據。
能夠類比銀行的保險櫃,好比你要找歸屬你的保險櫃子。若是沒有索引,你須要拿着鑰匙,一個個的保險櫃的試過去才能找到屬於你的保險櫃。可是若是有了索引,並且保險櫃可以以物理分區的方式存在在對應的區域,同時你能夠根據鑰匙上的編號(A1003-10-17),找到保險櫃所在 A1003的存放房間,找到存放室保險櫃的第10排,再找到第17個位置,找到屬於你的保險櫃,這個定位就快不少了。在沒有索引的狀況下,要想完成這個事情仍是比較困難的。
索引的原理
除了保險櫃以外,生活中能夠引出不少相似的索引例子,如字典詞典的目錄、圖書館的檢索錄、火車的座次表等。
它們的原理一致:不斷的縮小數據範圍來篩選數據,並把隨機數據變成順序數據,方便咱們更快地鎖定數據。
這種索引的理解一樣適用咱們的數據庫查詢,可是數據庫會有不少更復雜的狀況,除了等值查詢外,還有範圍查詢(>、<、between、in)、模糊查詢(like)、並集查詢(or)、交集查詢(and)等等。這就要求數據庫選擇更加複雜和成熟的方式來應對全部問題。
根據咱們上面保險櫃的案例,能夠對數據按照必定規則進行拆分,這樣匹配的範圍就下降了,可是這遠遠不夠知足數據庫複雜的查詢要求。因而,數據庫系統的設計者從查詢算法的角度進行優化。
其中最基本的查詢算法是順序查找(linear search),這種算法複雜度爲O(n),在數據量很大時就很不理想了,並且數據量越大,計算越複雜。
但不要緊,強大的計算機科學提供了更多優秀的查找算法,好比二分查找(binary search)、二叉樹查找(binary tree search)等。
可是這些查找算法都要求應用於特定的數據結構之上,如二分查找要求被檢索數據有序,而二叉樹查找只能基於二叉查找樹結構上操做,數據自己的組織結構不可能徹底知足各類數據結構,理論上也沒法同時要求將多列都按順序進行組織。
所以,
在數據以外,數據庫系統還維護着知足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就能夠在這些數據結構上實現高級查找算法。這種數據結構,就是索引。
這與上面MySQL官方對索引的定義遙相呼應了。
看下面的圖:
圖舉例了一種索引方式。右邊是一個數據表,這邊一共模擬了兩列七行的數據,
字段1的是數據記錄的物理地址(實際應用中邏輯上相鄰的記錄在磁盤上並不必定物理相鄰,這邊主要爲了舉例)。爲了加快
字段2的查找,能夠維護一個左邊所示的二叉查找樹,每一個節點分別包含索引鍵值和一個指向對應數據記錄物理地址的指針,這樣就能夠運用二叉查找在O(log2n)O(log2n)的複雜度內獲取到相應數據。
這是索引的一種表現形式,可是實際的數據庫系統中比較廣泛是採用B+樹來實現的。B+樹中的B表明平衡(balance),不是二叉(binary)。由於B+樹是從最先的平衡二叉樹演化而來的,因此咱們能夠先了解下二叉查找樹、平衡二叉樹(AVLTree)和平衡多路查找樹(B-Tree),由於B+樹是由這些樹逐步演進而來。
二叉查找樹
二叉樹具備如下性質:左子樹的鍵值小於根的鍵值,右子樹的鍵值大於根的鍵值。 因此左中右是依次遞增的一個過程。
以下圖所示就是一棵二叉查找樹,
觀察該二叉樹有有以下發現,深度爲1的節點的查找次數爲1,深度爲2的查找次數爲2,深度爲n的節點的查找次數爲n,所以其平均查找次數爲 (1+2+2+3+3+3+3) / 7 = 2.4次。
二叉查找樹也能夠是以下結構(一樣知足二叉樹 左 < 中 < 大的特性),一樣是7,21,35,42,51,77,89 這七個數字,也能夠按照下圖的方式來構造:
可是這棵二叉樹的查詢效率就低了,平均查找次數爲(1+2+3+4+5+6+6)/7=3.8次。
所以若想二叉樹的查詢效率儘量高,須要這棵二叉樹是平衡的,從而引出新的定義:AVL樹(即平衡二叉樹)。
平衡二叉樹(AVL Tree)
平衡二叉樹(AVL樹)在符合二叉查找樹的條件下,
還知足任何節點的兩個子樹的高度最大差爲1。下面的兩張圖片,左邊是AVL樹,它的任何節點的兩個子樹的高度差<=1;右邊的不是AVL樹,其根節點的左子樹高度爲3,而右子樹高度爲1,高度差>1;
同理,在平衡二叉樹進行插入或刪除節點,也可能致使AVL樹失去平衡,這種失去平衡的二叉樹能夠有四種狀態:LL(左左)、RR(右右)、LR(左右)、RL(右左)。
看下圖示:
咱們來逐一看下這幾種狀態。
LL(LeftLeft),即 左左。是指插入或刪除一個節點後,根節點的左孩子(Left Child)的左孩子(Left Child)還有非空節點,致使根節點的左子樹比右子樹高度>1,AVL樹失去平衡。
RR(RightRight),即 右右。是指插入或刪除一個節點後,根節點的右孩子(Right Child)的右孩子(Right Child)還有非空節點,致使根節點的右子樹比左子樹高度>1,AVL樹失去平衡。
LR(LeftRight),即 左右。插入或刪除一個節點後,根節點的左孩子(Left Child)的右孩子(Right Child)還有非空節點,致使根節點的左子樹比右子樹高度>1,AVL樹失去平衡。
RL(RightLeft),即 右左。插入或刪除一個節點後,根節點的右孩子(Right Child)的左孩子(Left Child)還有非空節點,致使根節點的右子樹比左子樹高度>1,AVL樹失去平衡。
失去平衡的AVL樹,能夠經過旋轉來修復,旋轉的本質是將樹的節點進行調整,達到恢復平衡的目的。下面逐一來看下。
LL的旋轉:LL失去平衡的狀況下,能夠經過一次旋轉讓AVL樹恢復平衡。步驟以下:
一、將根節點的左孩子做爲新根節點。
二、將新根節點的右孩子做爲原根節點的左孩子。
三、將原根節點做爲新根節點的右孩子。
以下圖所示:
RR的旋轉:RR失去平衡的狀況下,旋轉方法與LL旋轉相反,步驟以下:
一、將根節點的右孩子做爲新根節點。
二、將新根節點的左孩子做爲原根節點的右孩子。
三、將原根節點做爲新根節點的左孩子。
以下圖所示:
LR的旋轉:LR失去平衡的狀況下,須要進行兩次旋轉,步驟以下:
一、圍繞根節點的左孩子進行RR旋轉。
二、圍繞根節點進行LL旋轉。
以下圖所示,它轉了兩次,最後恢復成一棵AVL樹:
RL的旋轉:RL失去平衡的狀況下也須要進行兩次旋轉,旋轉方法與LR旋轉相反,步驟以下:
一、圍繞根節點的右孩子進行LL旋轉。
二、圍繞根節點進行RR旋轉。
以下圖所示,它轉了兩次,最後恢復成一棵AVL樹:
平衡多路查找樹(B-Tree)
咱們知道,磁盤這種存儲設備是以磁盤塊(block)爲基本單位的,而B-樹也是基於這種存儲方式設計的平衡查找樹。
因此當咱們從系統磁盤讀取數據時,以磁盤塊(block)爲基本單位映射到內存中,位於同一個磁盤塊中的數據會被一次性讀取出來,而不是隻取須要的數據。InnoDB存儲引擎中有頁(Page)的概念,頁是其磁盤管理的最小單位。InnoDB存儲引擎中默認每一個頁的大小爲16KB,可經過參數innodb_page_size將頁的大小設置爲4K、8K、16K,咱們能夠在命令窗口輸入如下腳本查看:
1 mysql> show variables like 'innodb_page_size';
2 +------------------+-------+
3 | Variable_name | Value |
4 +------------------+-------+
5 | innodb_page_size | 16384 |
6 +------------------+-------+
7 1 row in set
而系統一個磁盤塊的存儲空間每每沒有這麼大,所以InnoDB每次申請磁盤空間時都會是若干地址連續磁盤塊來達到頁的大小16KB。
InnoDB在把磁盤數據讀入到磁盤時會以頁爲基本單位,在查詢數據時若是一個頁中的每條數據都能有助於定位數據記錄的位置,
這將會減小磁盤I/O次數,提升查詢效率。
B-Tree結構的數據可讓系統高效的找到數據所在的磁盤塊。爲了描述B-Tree,首先定義一條記錄爲一個二元組[key, data] ,key爲記錄的鍵值,對應表中的主鍵值,data爲一行記錄中除主鍵外的數據。對於不一樣的記錄,key值互不相同。
一棵m階的B-Tree有以下特性:
1. 每一個節點最多有m個孩子。
2. 除了根節點和葉子節點外,其它每一個節點至少有Ceil(m/2)個孩子。
3. 若根節點不是葉子節點,則至少有2個孩子
4. 全部葉子節點都在同一層,且不包含其它關鍵字信息
5. 每一個非終端節點包含n個關鍵字信息(P0,P1,…Pn, k1,…kn)
6. 關鍵字的個數n知足:ceil(m/2)-1 <= n <= m-1
7. ki(i=1,…n)爲關鍵字,且關鍵字升序排序。
8. Pi(i=1,…n)爲指向子樹根節點的指針。P(i-1)指向的子樹的全部節點關鍵字均小於ki,但都大於k(i-1)
B-Tree中的每一個節點根據實際狀況能夠包含大量的關鍵字信息和分支,以下圖所示爲一個3階的B-Tree:
每一個節點佔用一個盤塊的磁盤空間,一個節點上有兩個升序排序的關鍵字和三個指向子樹根節點的指針,指針存儲的是子節點所在磁盤塊的地址。兩個鍵值數據劃分紅的三個範圍域對應三個指針指向的子樹的數據的範圍域。以根節點爲例,兩個鍵值數據爲33和66,P1指針指向的子樹的數據範圍爲小於33,P2指針指向的子樹的數據範圍爲33~66之間,P3指針指向的子樹的數據範圍爲大於66。
模擬查找關鍵字55的過程:
一、根據根節點找到磁盤塊Disk1,讀入內存。第1次操做磁盤I/O。
二、比較鍵值55在區間(33,66),找到磁盤塊Disk1的指針P2。
三、根據P2指針找到磁盤塊Disk3,讀入內存。第2次操做磁盤I/O。
四、比較鍵值55在區間(39,62),找到磁盤塊Disk3的指針P2。
五、根據P2指針找到磁盤塊Disk8,讀入內存。第3次操做磁盤I/O。
六、在Disk8中的鍵值列表中找到關鍵字55。
經過上面的操做過程,發現須要3次磁盤I/O操做,和3次內存查找操做。因爲內存中的關鍵字是一個有序表結構,能夠利用二分法查找提升效率。而3次磁盤I/O操做是影響整個B-Tree查找效率的決定因素。
B-Tree相對於AVLTree縮減了節點個數,使每次磁盤I/O取到內存的數據都發揮了做用,從而提升了查詢效率。
B+Tree
B+Tree是在B-Tree基礎上的一種優化,使其更適合實現外存儲索引結構,InnoDB存儲引擎就是用B+Tree實現其索引結構。
從上面的B-Tree結構圖中能夠看到每一個節點中不只包含數據的key值,還有data值。而每個頁的存儲空間是有限的,若是data數據較大時將會致使每一個節點(即一個頁)能存儲的key的數量很小,當存儲的數據量很大時一樣會致使B-Tree的深度較大,增大查詢時的磁盤I/O次數,進而影響查詢效率。在B+Tree中,全部數據記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣能夠大大加大每一個節點存儲的key值數量,下降B+Tree的高度,提升查找效率。
B+Tree相比較於B-Tree的不一樣點:
一、非葉子節點只存儲鍵值信息。
二、全部葉子節點之間都有一個鏈指針。
三、數據記錄都存放在葉子節點中。
將上面的B-Tree優化,因爲B+Tree的非葉子節點只存儲鍵值信息,假設每一個磁盤塊能存儲4個鍵值及指針信息,則變成B+Tree後其結構以下圖所示:
一般在B+Tree上有兩個頭指針,一個指向根節點,另外一個指向關鍵字最小的葉子節點,並且全部葉子節點(即數據節點)之間是一種鏈式環結構。所以能夠對B+Tree進行兩種查找運算:一種是對於主鍵的範圍查找和分頁查找,另外一種是從根節點開始,進行隨機查找。
可能上面例子中只有22條數據記錄,看不出B+Tree的優勢,下面作一個推算:
InnoDB存儲引擎中頁的大小爲16KB,通常表的主鍵類型爲INT(佔用4個字節)或BIGINT(佔用8個字節),指針類型也通常爲4或8個字節,也就是說一個頁(B+Tree中的一個節點)中大概存儲16KB/(8B+8B)=1K個鍵值(由於是估值,爲方便計算,這裏的K取值爲〖10〗^3)。也就是說一個深度爲3的B+Tree索引能夠維護10^3 * 10^3 * 10^3 = 10億 條記錄。
實際狀況中每一個節點可能不能填充滿,所以在數據庫中,B+Tree的高度通常都在2~4層。mysql的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多隻須要1~3次磁盤I/O操做。
數據庫中的B+Tree索引能夠分爲彙集索引(clustered index)和輔助索引(secondary index)。上面的B+Tree示例圖在數據庫中的實現即爲彙集索引,彙集索引的B+Tree中的葉子節點存放的是整張表的行記錄數據。輔助索引與彙集索引的區別在於輔助索引的葉子節點並不包含行記錄的所有數據,而是存儲相應行數據的彙集索引鍵,即主鍵。當經過輔助索引來查詢數據時,InnoDB存儲引擎會遍歷輔助索引找到主鍵,而後再經過主鍵在彙集索引中找到完整的行記錄數據。
總結
根據上面,二叉查找樹,紅黑樹等數據結構也能夠用來實現索引,可是文件系統及數據庫系統廣泛採用B+Tree做爲索引結構(
目前MySQL的MYISAM 和 INNODB 都是採用B+Tree做爲索引結構),這是由於B+Tree索引的設計是以計算機磁盤存儲結構爲理論基礎的。
索引以索引文件的形式存儲在磁盤上,當採用B+Tree查找的時候,產生磁盤I/O消耗對性能的影響比其餘方式小不少(
評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤I/O操做次數的漸進複雜度)。
換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數,而B+Tree無疑是較優的算法。