先來一段有莫的對話:html
前幾天下班回到家後正在處理一個白天沒解決的bug,廁所忽然傳來對象的聲音:mysql
對象:xx,你有《時間簡史》嗎?
我:我去!妹子,你這啥癖好啊,我有時間也不會去撿屎啊!
對象:...人家說的是霍金的科普著做《時間簡史》,是一本書啦!
我:哦,那我沒有...
對象:人家想看誒,你明天幫我去圖書館借一本吧...
我:我明天還要改...
對象:你是否是不愛我了,分手!
我:我一大早就去~sql
次日一大早我就到了圖書館,剛進門就看到一個索引牌,標識着不一樣樓層的功能,這樣我很快能定位到我要找的目標所在的樓層了。數據庫
我到樓上後又看到每排的書架上又對書的分類進行了細分,這樣我能更快的定位到我要找的書具體在哪一個書架!數組
而且每一個樓層都有一臺查詢終端,輸入書名就能查到對應的惟一標識「索書號」,相似於P159-49/164這樣的一個編碼,書架上的書都是按照這個編碼進行排序的!有了這個編碼再去對應的書架上,很快就能找到對應的書在書架的具體位置了。網絡
不到十分鐘,我就從圖書館借好書出來了。數據結構
這麼大的圖書館,我爲何能在這麼短的時間內找到我要的書?若是這些書是雜亂無章的堆放,或者沒有任何標識的放在書架,我還能這麼快的找到嗎?oracle
這不由讓我想到了咱們開發中用到的數據庫,圖書館的書就相似咱們數據表中的數據,樓層索引牌、書架分類標識、索書號就相似咱們查找數據的索引。sqlserver
那咱們經常使用的數據庫的索引底層的一個數據結構是什麼樣的呢?想到這裏我又回到圖書館借了一本《數據庫從入門到放棄》!性能
要了解數據庫索引的底層原理,咱們就得先了解一種叫樹的數據結構,而樹中很經典的一種數據結構就是二叉樹!因此下面咱們就從二叉樹到平衡二叉樹,再到B-樹,最後到B+樹來一步一步瞭解數據庫索引底層的原理!
二叉樹是每一個結點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)。二叉樹常被用於實現二叉查找樹和二叉堆。二叉樹有以下特性:
一、每一個結點都包含一個元素以及n個子樹,這裏0≤n≤2。
二、左子樹和右子樹是有順序的,次序不能任意顛倒。左子樹的值要小於父結點,右子樹的值要大於父結點。
光看概念有點枯燥,假設咱們如今有這樣一組數[35 27 48 12 29 38 55],順序的插入到一個數的結構中,步驟以下
好了,這就是一棵二叉樹啦!咱們能看到,經經過一系列的插入操做以後,本來無序的一組數已經變成一個有序的結構了,而且這個樹知足了上面提到的兩個二叉樹的特性!
可是若是一樣是上面那一組數,咱們本身升序排列後再插入,也就是說按照[12 27 29 35 38 48 55]的順序插入,會怎麼樣呢?
因爲是升序插入,新插入的數據老是比已存在的結點數據都要大,因此每次都會往結點的右邊插入,最終致使這棵樹嚴重偏科!!!上圖就是最壞的狀況,也就是一棵樹退化爲一個線性鏈表了,這樣查找效率天然就低了,徹底沒有發揮樹的優點了呢!
爲了較大發揮二叉樹的查找效率,讓二叉樹再也不偏科,保持各科平衡,因此有了平衡二叉樹!
平衡二叉樹是一種特殊的二叉樹,因此他也知足前面說到的二叉樹的兩個特性,同時還有一個特性:
它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹。
你們也看到了前面[35 27 48 12 29 38 55]插入完成後的圖,其實就已是一顆平衡二叉樹啦。
那若是按照[12 27 29 35 38 48 55]的順序插入一顆平衡二叉樹,會怎麼樣呢?咱們看看插入以及平衡的過程:
這棵樹始終知足平衡二叉樹的幾個特性而保持平衡!這樣咱們的樹也不會退化爲線性鏈表了!咱們須要查找一個數的時候就能沿着樹根一直往下找,這樣的查找效率和二分法查找是同樣的呢!
一顆平衡二叉樹能容納多少的結點呢?這跟樹的高度是有關係的,假設樹的高度爲h,那每一層最多容納的結點數量爲2^(n-1),整棵樹最多容納節點數爲2^0+2^1+2^2+...+2^(h-1)。這樣計算,100w數據樹的高度大概在20左右,那也就是說從有着100w條數據的平衡二叉樹中找一個數據,最壞的狀況下須要20次查找。若是是內存操做,效率也是很高的!可是咱們數據庫中的數據基本都是放在磁盤中的,每讀取一個二叉樹的結點就是一次磁盤IO,這樣咱們找一條數據若是要通過20次磁盤的IO?那性能就成了一個很大的問題了!那咱們是否是能夠把這棵樹壓縮一下,讓每一層可以容納更多的節點呢?雖然我矮,可是我胖啊...
這顆矮胖的樹就是B-Tree,注意中間是槓精的槓而不是減,因此也不要讀成B減Tree了~
那B-Tree有哪些特性呢?一棵m階的B-Tree有以下特性:
一、每一個結點最多m個子結點。
二、除了根結點和葉子結點外,每一個結點最少有m/2(向上取整)個子結點。
三、若是根結點不是葉子結點,那根結點至少包含兩個子結點。
四、全部的葉子結點都位於同一層。
五、每一個結點都包含k個元素(關鍵字),這裏m/2≤k<m,這裏m/2向下取整。
六、每一個節點中的元素(關鍵字)從小到大排列。
七、每一個元素(關鍵字)字左結點的值,都小於或等於該元素(關鍵字)。右結點的值都大於或等於該元素(關鍵字)。
是否是感受跟丈母孃張口問你要彩禮同樣,列一堆的條件,並且每一條都讓你很懵逼!下面咱們以一個[0,1,2,3,4,5,6,7]的數組插入一顆3階的B-Tree爲例,將全部的條件都串起來,你就明白了!
那麼,你是否對B-Tree的幾點特性都清晰了呢?在二叉樹中,每一個結點只有一個元素。可是在B-Tree中,每一個結點均可能包含多個元素,而且非葉子結點在元素的左右都有指向子結點的指針。
若是須要查找一個元素,那流程是怎麼樣的呢?咱們看下圖,若是咱們要在下面的B-Tree中找到關鍵字24,那流程以下
從這個流程咱們能看出,B-Tree的查詢效率好像也並不比平衡二叉樹高。可是查詢所通過的結點數量要少不少,也就意味着要少不少次的磁盤IO,這對
性能的提高是很大的。
前面對B-Tree操做的圖咱們能看出來,元素就是相似一、二、3這樣的數值,可是數據庫的數據都是一條條的數據,若是某個數據庫以B-Tree的數據結構存儲數據,那數據怎麼存放的呢?咱們看下一張圖
普通的B-Tree的結點中,元素就是一個個的數字。可是上圖中,咱們把元素部分拆分紅了key-data的形式,key就是數據的主鍵,data就是具體的數據。這樣咱們在找一條數的時候,就沿着根結點往下找就ok了,效率是比較高的。
B+Tree是在B-Tree基礎上的一種優化,使其更適合實現外存儲索引結構。B+Tree與B-Tree的結構很像,可是也有幾個本身的特性:
一、全部的非葉子節點只存儲關鍵字信息。
二、全部衛星數據(具體數據)都存在葉子結點中。
三、全部的葉子結點中包含了所有元素的信息。
四、全部葉子節點之間都有一個鏈指針。
若是上面B-Tree的圖變成B+Tree,那應該以下:
你們仔細對比於B-Tree的圖能發現什麼不一樣?
一、非葉子結點上已經只有key信息了,知足上面第1點特性!
二、全部葉子結點下面都有一個data區域,知足上面第2點特性!
三、非葉子結點的數據在葉子結點上都能找到,如根結點的元素四、8在最底層的葉子結點上也能找到,知足上面第3點特性!
四、注意圖中葉子結點之間的箭頭,知足知足上面第4點特性!
在講這兩種數據結構在數據庫中的選擇以前,咱們還須要瞭解的一個知識點是操做系統從磁盤讀取數據到內存是以磁盤塊(block)爲基本單位的,位於同一個磁盤塊中的數據會被一次性讀取出來,而不是須要什麼取什麼。即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存。這樣作的理論依據是計算機科學中著名的局部性原理: 當一個數據被用到時,其附近的數據也一般會立刻被使用。
預讀的長度通常爲頁(page)的整倍數。頁是計算機管理存儲器的邏輯塊,硬件及操做系統每每將主存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(在許多操做系統中,頁得大小一般爲4k)。
B-Tree和B+Tree該如何選擇呢?都有哪些優劣呢?
一、B-Tree由於非葉子結點也保存具體數據,因此在查找某個關鍵字的時候找到便可返回。而B+Tree全部的數據都在葉子結點,每次查找都獲得葉子結點。因此在一樣高度的B-Tree和B+Tree中,B-Tree查找某個關鍵字的效率更高。
二、因爲B+Tree全部的數據都在葉子結點,而且結點之間有指針鏈接,在找大於某個關鍵字或者小於某個關鍵字的數據的時候,B+Tree只須要找到該關鍵字而後沿着鏈表遍歷就能夠了,而B-Tree還須要遍歷該關鍵字結點的根結點去搜索。
三、因爲B-Tree的每一個結點(這裏的結點能夠理解爲一個數據頁)都存儲主鍵+實際數據,而B+Tree非葉子結點只存儲關鍵字信息,而每一個頁的大小有限是有限的,因此同一頁能存儲的B-Tree的數據會比B+Tree存儲的更少。這樣一樣總量的數據,B-Tree的深度會更大,增大查詢時的磁盤I/O次數,進而影響查詢效率。
鑑於以上的比較,因此在經常使用的關係型數據庫中,都是選擇B+Tree的數據結構來存儲數據!下面咱們以mysql的innodb存儲引擎爲例講解,其餘相似sqlserver、oracle的原理相似!
在InnoDB存儲引擎中,也有頁的概念,默認每一個頁的大小爲16K,也就是每次讀取數據時都是讀取4*4k的大小!假設咱們如今有一個用戶表,咱們往裏面寫數據
這裏須要注意的一點是,在某個頁內插入新行時,爲了避免減小數據的移動,一般是插入到當前行的後面或者是已刪除行留下來的空間,因此在某一個頁內的數據並不是徹底有序的(後面頁結構部分有細講),可是爲了爲了數據訪問順序性,在每一個記錄中都有一個指向下一條記錄的指針,以此構成了一條單向有序鏈表,不過在這裏爲了方便演示我是按順序排列的!
因爲數據還比較少,一個頁就能容下,因此只有一個根結點,主鍵和數據也都是保存在根結點(左邊的數字表明主鍵,右邊名字、性別表明具體的數據)。假設咱們寫入10條數據以後,Page1滿了,再寫入新的數據會怎麼存放呢?咱們繼續看下圖
有個叫「秦壽生」的朋友來了,可是Page1已經放不下數據了,這時候就須要進行頁分裂,產生一個新的Page。在innodb中的流程是怎麼樣的呢?
一、產生新的Page2,而後將Page1的內容複製到Page2。
二、產生新的Page3,「秦壽生」的數據放入Page3。
三、原來的Page1依然做爲根結點,可是變成了一個不存放數據只存放索引的頁,而且有兩個子結點Page二、Page3。
這裏有兩個問題須要注意的是
一、爲何要複製Page1爲Page2而不是建立一個新的頁做爲根結點,這樣就少了一步複製的開銷了?
若是是從新建立根結點,那根結點存儲的物理地址可能常常會變,不利於查找。而且在innodb中根結點是會預讀到內存中的,因此結點的物理地址固定會比較好!
二、原來Page1有10條數據,在插入第11條數據的時候進行裂變,根據前面對B-Tree、B+Tree特性的瞭解,那這至少是一顆11階的樹,裂變以後每一個結點的元素至少爲11/2=5個,那是否是應該頁裂變以後主鍵1-5的數據仍是在原來的頁,主鍵6-11的數據會放到新的頁,根結點存放主鍵6?
若是是這樣的話新的頁空間利用率只有50%,而且會致使更爲頻繁的頁分裂。因此innodb對這一點作了優化,新的數據放入新建立的頁,不移動原有頁面的任何記錄。
隨着數據的不斷寫入,這棵樹也逐漸枝繁葉茂,以下圖
每次新增數據,都是將一個頁寫滿,而後新建立一個頁繼續寫,這裏實際上是有個隱含條件的,那就是主鍵自增!主鍵自增寫入時新插入的數據不會影響到原有頁,插入效率高!且頁的利用率高!可是若是主鍵是無序的或者隨機的,那每次的插入可能會致使原有頁頻繁的分裂,影響插入效率!下降頁的利用率!這也是爲何在innodb中建議設置主鍵自增的緣由!
這棵樹的非葉子結點上存的都是主鍵,那若是一個表沒有主鍵會怎麼樣?在innodb中,若是一個表沒有主鍵,那默認會找建了惟一索引的列,若是也沒有,則會生成一個隱形的字段做爲主鍵!
有數據插入那就有刪除,若是這個用戶表頻繁的插入和刪除,那會致使數據頁產生碎片,頁的空間利用率低,還會致使樹變的「虛高」,下降查詢效率!這能夠經過索引重建來消除碎片提升查詢效率!
數據插入了怎麼查找呢?
一、找到數據所在的頁。這個查找過程就跟前面說到的B+Tree的搜索過程是同樣的,從根結點開始查找一直到葉子結點。
二、在頁內找具體的數據。讀取第1步找到的葉子結點數據到內存中,而後經過分塊查找的方法找到具體的數據。
這跟咱們在新華字典中找某個漢字是同樣的,先經過字典的索引定位到該漢字拼音所在的頁,而後到指定的頁找到具體的漢字。innodb中定位到頁後用了哪一種策略快速查找某個主鍵呢?這咱們就須要從頁結構開始瞭解。
左邊藍色區域稱爲Page Directory,這塊區域由多個slot組成,是一個稀疏索引結構,即一個槽中可能屬於多個記錄,最少屬於4條記錄,最多屬於8條記錄。槽內的數據是有序存放的,因此當咱們尋找一條數據的時候能夠先在槽中經過二分法查找到一個大體的位置。
右邊區域爲數據區域,每個數據頁中都包含多條行數據。注意看圖中最上面和最下面的兩條特殊的行記錄Infimum和Supremum,這是兩個虛擬的行記錄。在沒有其餘用戶數據的時候Infimum的下一條記錄的指針指向Supremum,當有用戶數據的時候,Infimum的下一條記錄的指針指向當前頁中最小的用戶記錄,當前頁中最大的用戶記錄的下一條記錄的指針指向Supremum,至此整個頁內的全部行記錄造成一個單向鏈表。
行記錄被Page Directory邏輯的分紅了多個塊,塊與塊之間是有序的,也就是說「4」這個槽指向的數據塊內最大的行記錄的主鍵都要比「8」這個槽指向的數據塊內最小的行記錄的主鍵要小。可是塊內部的行記錄不必定有序。
每一個行記錄的都有一個n_owned的區域(圖中粉紅色區域),n_owned標識這個這個塊有多少條數據,僞記錄Infimum的n_owned值老是1,記錄Supremum的n_owned的取值範圍爲[1,8],其餘用戶記錄n_owned的取值範圍[4,8],而且只有每一個塊中最大的那條記錄的n_owned纔會有值,其餘的用戶記錄的n_owned爲0。
因此當咱們要找主鍵爲6的記錄時,先經過二分法在稀疏索引中找到對應的槽,也就是Page Directory中「8」這個槽,「8」這個槽指向的是該數據塊中最大的記錄,而數據是單向鏈表結構因此沒法逆向查找,因此須要找到上一個槽即「4」這個槽,而後經過「4」這個槽中最大的用戶記錄的指針沿着鏈表順序查找到目標記錄。
前面關於數據存儲的都是演示的彙集索引的實現,若是上面的用戶表須要以「用戶名字」創建一個非彙集索引,是怎麼實現的呢?咱們看下圖:
非彙集索引的存儲結構與前面是同樣的,不一樣的是在葉子結點的數據部分存的再也不是具體的數據,而數據的彙集索引的key。因此經過非彙集索引查找的過程是先找到該索引key對應的彙集索引的key,而後再拿彙集索引的key到主鍵索引樹上查找對應的數據,這個過程稱爲回表!
圖中的這些名字均來源於網絡,但願沒有誤傷正在看這篇文章的你~^_^
上面包括存儲和搜索都是拿的innodb引擎爲例,那MyISAM與innodb在存儲上有啥不一樣呢?憋縮話,看圖:
上圖爲MyISAM主鍵索引的存儲結構,咱們能看到的不一樣是
一、主鍵索引樹的葉子結點的數據區域沒有存放實際的數據,存放的是數據記錄的地址。
二、數據的存儲不是按主鍵順序存放的,按寫入的順序存放。
也就是說innodb引擎數據在物理上是按主鍵順序存放,而MyISAM引擎數據在物理上按插入的順序存放。而且MyISAM的葉子結點不存放數據,因此非彙集索引的存儲結構與彙集索引相似,在使用非彙集索引查找數據的時候經過非彙集索引樹就能直接找到數據的地址了,不須要回表,這比innodb的搜索效率會更高呢!
你們常常會在不少的文章或書中能看到一些索引的使用建議,好比說
一、like的模糊查詢以%開頭,會致使索引失效。
二、一個表建的索引儘可能不要超過5個。
三、儘可能使用覆蓋索引。
四、儘可能不要在重複數據多的列上建索引。
五、。。。。。。。。。。。
六、。。。。。。。。。。。
不少這裏就不一一列舉了!那看完這篇文章,咱們可否帶着疑問去分析一下爲何要有這些建議?爲何like的模糊查詢以%開頭,會致使索引失效?爲何一個表建的索引儘可能不要超過5個?爲何? 爲何??爲何???相信看到這裏的你再加上本身的一些思考應該有答案了吧?