MySQL索引背後的數據結構及算法原理【轉】

http://blog.codinglabs.org/articles/theory-of-mysql-index.html
MySQL索引背後的數據結構及算法原理【轉】
摘要
本文以MySQL數據庫爲研究對象,討論與數據庫索引相關的一些話題。特別須要說明的是,MySQL支持諸多存儲引擎,而各類存儲引擎對索引的支持也各不相同,所以MySQL數據庫支持多種索引類型,如BTree索引,哈希索引,全文索引等等。爲了不混亂,本文將只關注於BTree索引,由於這是日常使用MySQL時主要打交道的索引,至於哈希索引和全文索引本文暫不討論。html

文章主要內容分爲三個部分。node

第一部分主要從數據結構及算法理論層面討論MySQL數據庫索引的數理基礎。mysql

第二部分結合MySQL數據庫中MyISAM和InnoDB數據存儲引擎中索引的架構實現討論彙集索引、非彙集索引及覆蓋索引等話題。算法

第三部分根據上面的理論基礎,討論MySQL中高性能使用索引的策略。sql

數據結構及算法基礎
索引的本質
MySQL官方對索引的定義爲:索引(Index)是幫助MySQL高效獲取數據的數據結構。提取句子主幹,就能夠獲得索引的本質:索引是數據結構。數據庫

咱們知道,數據庫查詢是數據庫的最主要功能之一。咱們都但願查詢數據的速度能儘量的快,所以數據庫系統的設計者會從查詢算法的角度進行優化。最基本的查詢算法固然是順序查找(linear search),這種複雜度爲O(n)的算法在數據量很大時顯然是糟糕的,好在計算機科學的發展提供了不少更優秀的查找算法,例如二分查找(binary search)、二叉樹查找(binary tree search)等。若是稍微分析一下會發現,每種查找算法都只能應用於特定的數據結構之上,例如二分查找要求被檢索數據有序,而二叉樹查找只能應用於二叉查找樹上,可是數據自己的組織結構不可能徹底知足各類數據結構(例如,理論上不可能同時將兩列都按順序進行組織),因此,在數據以外,數據庫系統還維護着知足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就能夠在這些數據結構上實現高級查找算法。這種數據結構,就是索引。
MySQL索引背後的數據結構及算法原理【轉】
圖1數據結構

圖1展現了一種可能的索引方式。左邊是數據表,一共有兩列七條記錄,最左邊的是數據記錄的物理地址(注意邏輯上相鄰的記錄在磁盤上也並非必定物理相鄰的)。爲了加快Col2的查找,能夠維護一個右邊所示的二叉查找樹,每一個節點分別包含自己的索引鍵值和一個能夠直接定位到數據的行號(指針)信息,這樣就能夠運用二叉查找在O(log2n)架構

雖然這是一個貨真價實的索引,可是實際的數據庫系統幾乎沒有使用二叉查找樹或其進化品種紅黑樹(red-black tree)實現的,緣由會在下文介紹。ide

B-Tree和B+Tree
目前大部分數據庫系統及文件系統都採用B-Tree或其變種B+Tree做爲索引結構,在本文的下一節會結合存儲器原理及計算機存取原理討論爲何B-Tree和B+Tree在被如此普遍用於索引,這一節先單純從數據結構角度描述它們。性能

B-Tree
爲了描述B-Tree,首先定義一條數據記錄爲一個二元組[key, data],key爲記錄的鍵值,對於不一樣數據記錄,key是互不相同的;data爲數據記錄除key外的數據。那麼B-Tree是知足下列條件的數據結構:

d爲大於1的一個正整數,稱爲B-Tree的度。

h爲一個正整數,稱爲B-Tree的高度。

每一個非葉子節點由n-1個key和n個指針組成,其中d<=n<=2d。

每一個葉子節點最少包含一個key和兩個指針,最多包含2d-1個key和2d個指針,葉節點的指針均爲null 。

全部葉節點具備相同的深度,等於樹高h。

key和指針互相間隔,節點兩端是指針。

一個節點中的key從左到右非遞減排列。

全部節點組成樹結構。

每一個指針要麼爲null,要麼指向另一個節點。

若是某個指針在節點node最左邊且不爲null,則其指向節點的全部key小於v(key1)

若是某個指針在節點node最右邊且不爲null,則其指向節點的全部key大於v(keym)

若是某個指針在節點node的左右相鄰key分別是keyi

圖2是一個d=2的B-Tree示意圖
MySQL索引背後的數據結構及算法原理【轉】
圖2
因爲B-Tree的特性,在B-Tree中按key檢索數據的算法很是直觀:首先從根節點進行二分查找,若是找到則返回對應節點的data,不然對相應區間的指針指向的節點遞歸進行查找,直到找到節點或找到null指針,前者查找成功,後者查找失敗。B-Tree上查找算法的僞代碼以下:

複製代碼
BTree_Search(node, key) {
if(node == null) return null;
foreach(node.key)
{
if(node.key[i] == key) return node.data[i];
if(node.key[i] > key) return BTree_Search(point[i]->node);
}
return BTree_Search(point[i+1]->node);
}
data = BTree_Search(root, my_key);
複製代碼
關於B-Tree有一系列有趣的性質,例如一個度爲d的B-Tree,設其索引N個key,則其樹高h的上限爲logd((N+1)/2)logd((N+1)/2),檢索一個key,其查找節點個數的漸進複雜度爲O(logdN)O(logdN)。從這點能夠看出,B-Tree是一個很是有效率的索引數據結構。

另外,因爲插入刪除新的數據記錄會破壞B-Tree的性質,所以在插入刪除時,須要對樹進行一個分裂、合併、轉移等操做以保持B-Tree性質,本文不打算完整討論B-Tree這些內容,由於已經有許多資料詳細說明了B-Tree的數學性質及插入刪除算法。

B+Tree
B-Tree有許多變種,其中最多見的是B+Tree,例如MySQL就廣泛使用B+Tree實現其索引結構。

與B-Tree相比,B+Tree有如下不一樣點:

每一個節點的指針上限爲2d而不是2d+1。

內節點不存儲data,只存儲key;葉子節點不存儲指針。

圖3是一個簡單的B+Tree示意
MySQL索引背後的數據結構及算法原理【轉】
圖3
因爲並非全部節點都具備相同的域,所以B+Tree中葉節點和內節點通常大小不一樣。這點與B-Tree不一樣,雖然B-Tree中不一樣節點存放的key和指針可能數量不一致,可是每一個節點的域和上限是一致的,因此在實現中B-Tree每每對每一個節點申請同等大小的空間。

通常來講,B+Tree比B-Tree更適合實現外存儲索引結構,具體緣由與外存儲器原理及計算機存取原理有關,將在下面討論。

帶有順序訪問指針的B+Tree
通常在數據庫系統或文件系統中使用的B+Tree結構都在經典B+Tree的基礎上進行了優化,增長了順序訪問指針。
MySQL索引背後的數據結構及算法原理【轉】
圖4
如圖4所示,在B+Tree的每一個葉子節點增長一個指向相鄰葉子節點的指針,就造成了帶有順序訪問指針的B+Tree。作這個優化的目的是爲了提升區間訪問的性能,例如圖4中若是要查詢key爲從18到49的全部數據記錄,當找到18後,只需順着節點和指針順序遍歷就能夠一次性訪問到全部數據節點,極大提到了區間查詢效率。

這一節對B-Tree和B+Tree進行了一個簡單的介紹,下一節結合存儲器存取原理介紹爲何目前B+Tree是數據庫系統實現索引的首選數據結構。

爲何使用B-Tree(B+Tree)
上文說過,紅黑樹等數據結構也能夠用來實現索引,可是文件系統及數據庫系統廣泛採用B-/+Tree做爲索引結構,這一節將結合計算機組成原理相關知識討論B-/+Tree做爲索引的理論基礎。

通常來講,索引自己也很大,不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗要高几個數量級,因此評價一個索引的優劣最重要的指標就是在查找過程當中磁盤I/O操做次數的漸進複雜度。換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的存取次數。下面先介紹內存和磁盤存取原理,而後再結合這些原理分析B-/+Tree做爲索引的效率。

主存存取原理
目前計算機使用的主存基本都是隨機讀寫存儲器(RAM),現代RAM的結構和存取原理比較複雜,這裏本文拋卻具體差異,抽象出一個十分簡單的存取模型來講明RAM的工做原理。
MySQL索引背後的數據結構及算法原理【轉】
圖5
從抽象角度看,主存是一系列的存儲單元組成的矩陣,每一個存儲單元存儲固定大小的數據。每一個存儲單元有惟一的地址,現代主存的編址規則比較複雜,這裏將其簡化成一個二維地址:經過一個行地址和一個列地址能夠惟必定位到一個存儲單元。圖5展現了一個4 x 4的主存模型。

內存的存取過程以下:

當須要從內存讀取數據時,系統將地址信號放到地址總線上傳給內存,內存讀到地址信號後,解析信號並定位到指定存儲單元,而後將此存儲單元數據放到數據總線上,返回供其它部件讀取。

寫內存的過程相似,系統將要寫入單元地址和數據分別放在地址總線和數據總線上,主存讀取兩個總線的內容,作相應的寫操做。

這裏能夠看出,內存存取的時間僅與存取次數呈線性關係,由於不存在機械操做,兩次存取的數據的「距離」不會對時間有任何影響,例如,先取A0再取A1和先取A0再取D3的時間消耗是同樣的。

磁盤存取原理
上文說過,索引通常以文件形式存儲在磁盤上,索引檢索須要磁盤I/O操做。與內存不一樣,磁盤I/O存在機械運動耗費,所以磁盤I/O的時間消耗是巨大的。

圖6是磁盤的總體結構示意圖。

MySQL索引背後的數據結構及算法原理【轉】
圖6
一個磁盤由大小相同且同軸的圓形盤片組成,磁盤能夠轉動(各個磁盤必須同步轉動)。在磁盤的一側有磁頭支架,磁頭支架固定了一組磁頭,每一個磁頭負責存取一個盤片的內容。磁頭不能轉動,可是能夠沿磁盤半徑方向運動(實際是斜切向運動),每一個磁頭同一時刻也必須是同軸的,即從正上方向下看,全部磁頭任什麼時候候都是重疊的(不過目前已經有多磁頭獨立技術,可不受此限制)。

圖7是磁盤結構的示意圖
MySQL索引背後的數據結構及算法原理【轉】
圖7
盤片被劃分紅一系列同心環,圓心是盤片中心,每一個同心環叫作一個磁道,全部半徑相同的磁道組成一個柱面。磁道被沿半徑線劃分紅一個個小的段,每一個段叫作一個扇區,每一個扇區是磁盤的最小存儲單元。爲了簡單起見,咱們下面假設磁盤只有一個盤片和一個磁頭。

當須要從磁盤讀取數據時,系統會將數據邏輯地址傳給磁盤,磁盤的控制電路按照尋址邏輯將邏輯地址翻譯成物理地址,即肯定要讀的數據在哪一個磁道,哪一個扇區。爲了讀取這個扇區的數據,須要將磁頭放到這個扇區上方,爲了實現這一點,而後磁頭須要移動對應磁道,這個過程叫作尋道,所耗費時間叫作尋道時間,而後磁盤旋轉將目標扇區旋轉到磁頭下,這個過程耗費的時間叫作旋轉時間。

局部性原理與磁盤預讀
因爲存儲介質的特性,磁盤自己存取就比內存慢不少,再加上機械運動耗費,磁盤的存取速度每每是內存的幾百分之一,所以爲了提升效率,要儘可能減小磁盤I/O。爲了達到這個目的,磁盤每每不是嚴格按需讀取,而是每次都會預讀,即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存。這樣作的理論依據是計算機科學中著名的局部性原理:當一個數據被用到時,其附近的數據也一般會立刻被使用。

程序運行期間所須要的數據一般比較集中,因爲磁盤順序讀取的效率很高(不須要尋道時間,只需不多的旋轉時間),所以對於具備局部性的程序來講,預讀能夠提升I/O效率。

預讀的長度通常爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操做系統每每將內存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(在許多操做系統中,頁得大小一般爲4k),內存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在內存中時,會觸發一個缺頁異常,向磁盤進行讀取,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,而後異常返回,程序繼續運行。

B-/+Tree索引的性能分析
到這裏終於能夠分析B-/+Tree索引的性能了。

上文說過通常使用磁盤I/O次數評價索引結構的優劣。先從B-Tree分析,根據B-Tree的定義,可知檢索一次最多須要訪問h個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次I/O就能夠徹底載入。爲了達到這個目的,在實際實現B-Tree還須要使用以下技巧:

每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了一個node只需一次I/O。

B-Tree中一次檢索最多須要h-1次I/O(根節點常駐內存),漸進複雜度爲O(h)=O(logdN)

綜上所述,用B-Tree做爲索引結構效率是很是高的。

而紅黑樹這種結構,h明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性,因此紅黑樹的I/O漸進複雜度也爲O(h),效率明顯比B-Tree差不少。

上文還說過,B+Tree更適合外存索引,緣由和內節點出度d有關。從上面分析能夠看到,d越大索引的性能越好,而出度的上限取決於節點內key和data的大小:

dmax=floor(pagesize/(keysize+datasize+pointsize))

floor表示向下取整。因爲B+Tree內節點去掉了data域,所以能夠擁有更大的出度,擁有更好的性能。

這一章從理論角度討論了與索引相關的數據結構與算法問題,下一章將討論B+Tree是如何具體實現爲MySQL中索引,同時將結合MyISAM和InnDB存儲引擎介紹非彙集索引和彙集索引兩種不一樣的索引實現形式。

MySQL索引實現
在MySQL中,索引屬於存儲引擎級別的概念,不一樣存儲引擎對索引的實現方式是不一樣的,本文主要討論MyISAM和InnoDB兩個存儲引擎的索引實現方式。

MyISAM索引實現
MyISAM引擎使用B+Tree做爲索引結構,葉節點的data域存放的是數據記錄的地址。下圖是MyISAM索引的原理圖:
MySQL索引背後的數據結構及算法原理【轉】
圖8
這裏設表一共有三列,假設咱們以Col1爲主鍵,則圖8是一個MyISAM表的主索引(Primary key)示意。能夠看出MyISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是惟一的,而輔助索引的key能夠重複。若是咱們在Col2上創建一個輔助索引,則此索引的結構以下圖所示:
MySQL索引背後的數據結構及算法原理【轉】
圖9
一樣也是一顆B+Tree,data域保存數據記錄的地址。

所以,MyISAM中索引的算法爲:首先按照B+Tree搜索算法搜索索引,若是指定的Key存在,則取出其data域的值,而後以data域的值爲地址,讀取相應數據記錄。總之,MyISAM的索引除了包含自己的鍵值信息,還包含了能夠直接定位到數據的行號(行指針)。即經過索引能夠直接經過行號(指針)找到數據。

MyISAM的索引方式也叫作「非彙集」的,之因此這麼稱呼是爲了與InnoDB的彙集索引區分。

InnoDB索引實現
雖然InnoDB也使用B+Tree做爲索引結構,但具體實現方式卻與MyISAM大相徑庭。

第一個重大區別是InnoDB的數據文件自己就是索引文件。從上文知道,MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件自己就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,所以InnoDB表數據文件自己就是主索引。
MySQL索引背後的數據結構及算法原理【轉】
圖10
圖10是InnoDB主索引(同時也是數據文件)的示意圖,能夠看到葉節點包含了完整的數據記錄,這種索引叫作彙集索引。由於InnoDB的數據文件自己要按主鍵彙集,因此InnoDB要求表必須有主鍵(MyISAM能夠沒有),若是沒有顯式指定,則MySQL系統會自動選擇一個能夠惟一標識數據記錄的列做爲主鍵,若是不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段做爲主鍵,這個字段長度爲6個字節,類型爲長×××。

第二個與MyISAM索引的不一樣是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的全部輔助索引都引用主鍵做爲data域。例如,圖11爲定義在Col3上的一個輔助索引:
MySQL索引背後的數據結構及算法原理【轉】
這裏以英文字符的ASCII碼做爲比較準則。彙集索引這種實現方式使得按主鍵的搜索十分高效,可是輔助索引搜索須要檢索兩遍索引(2次IO):首先檢索輔助索引得到主鍵,而後用主鍵到主索引中檢索得到記錄。

瞭解不一樣存儲引擎的索引實現方式對於正確使用和優化索引都很是有幫助,例如知道了InnoDB的索引實現後,就很容易明白爲何不建議使用過長的字段做爲主鍵,由於全部輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的字段做爲主鍵在InnoDB中不是個好主意,由於InnoDB數據文件自己是一顆B+Tree,非單調的主鍵會形成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂產生碎片,十分低效,而使用自增字段做爲主鍵則是一個很好的選擇。

更多的信息能夠繼續閱讀:張洋的MySQL索引背後的數據結構及算法原理

相關文章
相關標籤/搜索