MySQL索引相关的数据结构和算法
索引相關的數據結構和算法
通常我們所說的索引是指B-Tree索引,它是目前關系型數據庫中查找數據最為常用和有效的索引,大多數存儲引擎都支持這種索引。使用B-Tree這個術語,是因為MySQL在CREATE TABLE或其它語句中使用了這個關鍵字,但實際上不同的存儲引擎可能使用不同的數據結構,比如InnoDB就是使用的B+Tree。
B+Tree中的B是指balance,意為平衡。需要注意的是,B+樹索引并不能找到一個給定鍵值的具體行,它找到的只是被查找數據行所在的頁,接著數據庫會把頁讀入到內存,再在內存中進行查找,最后得到要查找的數據。
在介紹B+Tree前,先了解一下二叉查找樹,它是一種經典的數據結構,其左子樹的值總是小于根的值,右子樹的值總是大于根的值,如下圖①。如果要在這課樹中查找值為5的記錄,其大致流程:先找到根,其值為6,大于5,所以查找左子樹,找到3,而5大于3,接著找3的右子樹,總共找了3次。同樣的方法,如果查找值為8的記錄,也需要查找3次。所以二叉查找樹的平均查找次數為(3 + 3 + 3 + 2 + 2 + 1) / 6 = 2.3次,而順序查找的話,查找值為2的記錄,僅需要1次,但查找值為8的記錄則需要6次,所以順序查找的平均查找次數為:(1 + 2 + 3 + 4 + 5 + 6) / 6 = 3.3次,因此大多數情況下二叉查找樹的平均查找速度比順序查找要快。
由于二叉查找樹可以任意構造,同樣的值,可以構造出如圖②的二叉查找樹,顯然這棵二叉樹的查詢效率和順序查找差不多。若想二叉查找數的查詢性能最高,需要這棵二叉查找樹是平衡的,也即平衡二叉樹(AVL樹)。
平衡二叉樹首先需要符合二叉查找樹的定義,其次必須滿足任何節點的兩個子樹的高度差不能大于1。顯然圖②不滿足平衡二叉樹的定義,而圖①是一課平衡二叉樹。平衡二叉樹的查找性能是比較高的(性能最好的是最優二叉樹),查詢性能越好,維護的成本就越大。比如圖①的平衡二叉樹,當用戶需要插入一個新的值9的節點時,就需要做出如下變動。
通過一次左旋操作就將插入后的樹重新變為平衡二叉樹是最簡單的情況了,實際應用場景中可能需要旋轉多次。至此我們可以考慮一個問題,平衡二叉樹的查找效率還不錯,實現也非常簡單,相應的維護成本還能接受,為什么MySQL索引不直接使用平衡二叉樹?
隨著數據庫中數據的增加,索引本身大小隨之增加,不可能全部存儲在內存中,因此索引往往以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程中就要產生磁盤I/O消耗,相對于內存存取,I/O存取的消耗要高幾個數量級。可以想象一下一棵幾百萬節點的二叉樹的深度是多少?如果將這么大深度的一顆二叉樹放磁盤上,每讀取一個節點,需要一次磁盤的I/O讀取,整個查找的耗時顯然是不能夠接受的。那么如何減少查找過程中的I/O存取次數?
一種行之有效的解決方法是減少樹的深度,將二叉樹變為m叉樹(多路搜索樹),而B+Tree就是一種多路搜索樹。理解B+Tree時,只需要理解其最重要的兩個特征即可:第一,所有的關鍵字(可以理解為數據)都存儲在葉子節點(Leaf Page),非葉子節點(Index Page)并不存儲真正的數據,所有記錄節點都是按鍵值大小順序存放在同一層葉子節點上。其次,所有的葉子節點由指針連接。如下圖為高度為2的簡化了的B+Tree。
怎么理解這兩個特征?MySQL將每個節點的大小設置為一個頁的整數倍(原因下文會介紹),也就是在節點空間大小一定的情況下,每個節點可以存儲更多的內結點,這樣每個結點能索引的范圍更大更精確。所有的葉子節點使用指針鏈接的好處是可以進行區間訪問,比如上圖中,如果查找大于20而小于30的記錄,只需要找到節點20,就可以遍歷指針依次找到25、30。如果沒有鏈接指針的話,就無法進行區間查找。這也是MySQL使用B+Tree作為索引存儲結構的重要原因。
MySQL為何將節點大小設置為頁的整數倍,這就需要理解磁盤的存儲原理。磁盤本身存取就比主存慢很多,在加上機械運動損耗(特別是普通的機械硬盤),磁盤的存取速度往往是主存的幾百萬分之一,為了盡量減少磁盤I/O,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序向后讀取一定長度的數據放入內存,預讀的長度一般為頁的整數倍。
頁是計算機管理存儲器的邏輯塊,硬件及OS往往將主存和磁盤存儲區分割為連續的大小相等的塊,每個存儲塊稱為一頁(許多OS中,頁的大小通常為4K)。主存和磁盤以頁為單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置并向后連續讀取一頁或幾頁載入內存中,然后一起返回,程序繼續運行。
MySQL巧妙利用了磁盤預讀原理,將一個節點的大小設為等于一個頁,這樣每個節點只需要一次I/O就可以完全載入。為了達到這個目的,每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁里,加之計算機存儲分配都是按頁對齊的,就實現了讀取一個節點只需一次I/O。假設B+Tree的高度為h,一次檢索最多需要h-1次I/O(根節點常駐內存),復雜度O(h) = O(logmN)。實際應用場景中,M通常較大,常常超過100,因此樹的高度一般都比較小,通常不超過3。
最后簡單了解下B+Tree節點的操作,在整體上對索引的維護有一個大概的了解,雖然索引可以大大提高查詢效率,但維護索引仍要花費很大的代價,因此合理的創建索引也就尤為重要。
仍以上面的樹為例,我們假設每個節點只能存儲4個內節點。首先要插入第一個節點28,如下圖所示。
接著插入下一個節點70,在Index Page中查詢后得知應該插入到50 - 70之間的葉子節點,但葉子節點已滿,這時候就需要進行也分裂的操作,當前的葉子節點起點為50,所以根據中間值來拆分葉子節點,如下圖所示。
最后插入一個節點95,這時候Index Page和Leaf Page都滿了,就需要做兩次拆分,如下圖所示。
拆分后最終形成了這樣一顆樹。
B+Tree為了保持平衡,對于新插入的值需要做大量的拆分頁操作,而頁的拆分需要I/O操作,為了盡可能的減少頁的拆分操作,B+Tree也提供了類似于平衡二叉樹的旋轉功能。當Leaf Page已滿但其左右兄弟節點沒有滿的情況下,B+Tree并不急于去做拆分操作,而是將記錄移到當前所在頁的兄弟節點上。通常情況下,左兄弟會被先檢查用來做旋轉操作。就比如上面第二個示例,當插入70的時候,并不會去做頁拆分,而是左旋操作。
通過旋轉操作可以最大限度的減少頁分裂,從而減少索引維護過程中的磁盤的I/O操作,也提高索引維護效率。需要注意的是,刪除節點跟插入節點類似,仍然需要旋轉和拆分操作,這里就不再說明。
參考文章:https://www.jianshu.com/p/d7665192aaaf
總結
以上是生活随笔為你收集整理的MySQL索引相关的数据结构和算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL查询过程及Scheme设计与数
- 下一篇: ubantu18.04使用docker部