字節面試:談談索引爲何能提升查詢性能?

前言
昨天,有個女孩子問我提升數據庫查詢性能有什麼立竿見影的好方法?
這簡直是一道送分題,我自豪且略帶鄙夷的說,固然是加「索引」了。
她又不緊不慢的問,索引爲何就能提升查詢性能。
這還用問,索引就像一本書的目錄,用目錄查固然很快。
她失望地搖了搖頭,你說的只是一個類比,可爲何經過目錄就能提升查詢速度呢
唉,對啊,經過書目能夠快速查詢,這只是一個現象,真正緣由究竟是什麼呢。
那女孩看着詫異且表情僵硬的我,滿意而又意味深長的笑笑:原來你這個男程序員也不會,看來我還得靠本身研究了。
哎,熬夜又要憔悴了我這該死的美貌。
來自同行的羞辱,是可忍孰不可忍?!
因而,我踏上了數據庫索引學習的不歸路,原來數據庫索引使用了一種叫 B+ 樹的古老數據結構,固然也有 Hash 等類型,暫且不說,可 B+ 樹 這是個什麼妖魔鬼怪呢?
下面就來淺嘗輒止的扒一扒樹的前世此生。

正文
二叉樹
由 n( n > 0)個有限節點組成一個具備層次關係的集合,看起來就像一個倒掛的樹,所以稱這樣的數據結構爲樹。
一個節點的子節點個數叫作度,通俗的講就是樹叉的個數。樹中最大的度叫作樹的度,也叫作階。一個 2 階樹最多有 2 個子節點即最多有 2 叉,所以這樣的樹稱爲二叉樹,二叉樹是樹家族中最簡單的樹。
兩個叉的樹就是二叉樹,可這除了用來按必定結構存放數據外,跟查詢性能好像也不要緊,不會又是一個沒用的噱頭吧。

二分查找
據說二叉樹的原始威力來源於一種叫作二分查找的算法。
相傳在鸚鵡的原始社會,存在着森嚴的等級制度,每隻鳥必須按高矮順序分出等級和尊卑。
那麼問題來了,以下圖,怎樣才能找出最高最矮中等高的那些鸚鵡呢、以及指定高度的那隻呢?
第一種方法: 掃描法
一個一個依次測量,完畢後全部的問題都迎刃而解。
這種一個一個依次所有測量的方法叫作掃描,他的缺點很明顯,最高和最矮,須要所有測量完畢才能知曉。
而對於指定高度,最好的狀況是第一次就找到;最壞的狀況是最後一次才找到,時間複雜度爲 n,也就是說從 13 個鸚鵡中找到指定身高的那隻,最壞的狀況是查 13 次。
第二種方法:二分法
13 個鸚鵡所有聽令,按從矮到高列隊,向左看齊,報數。
報數字 1 的就是最矮的,報數字 13 的就是最高的,報數字 7 的就是中等身高的那隻。
最好和最壞的狀況都是一次找到。而查詢性能一會兒提升 13 倍,個人個乖乖,不管多個只鸚鵡,時間複雜度都是 1,好可怕。
問題:我不服,你這是偷換概念,有本事對比一個查找指定高度鸚鵡的性能。
由於鸚鵡們已經按高矮排好了隊,因此指定高度的鸚鵡,要麼是站中間那個只,要麼就是在它的左邊或右邊的那羣裏。
若是是中間那個,一次就找到,若是不是隻須要從中間左邊或右邊那一半中找,再在這一半中找中間那隻,對比身高。
以此類推,每次都把查詢的範圍減半,時間複雜度log2(n)
那麼 log2(13) 就是 4,最壞的狀況也才 4 次,時間複雜度確實不是 1 了,但好像也不糟,簡化以下:
問題:若是按高矮排隊,仍然須要一個一個比較,跟掃描有什麼區別,那還不如直接掃描呢?
事實確實如此,單純的一次查詢,先排序,再二分查找,不見得比掃描快,甚至還不如。
可是,在數據的世界,大部分數據一輩子會被查詢無數次,若是隻在數據降生的時候排一次序,日後餘生,是否是就能夠直接用二分查找,這彷佛就是傳說的讀多寫少,以及對應的複用。
優勢
  • 查找快
缺點:
  • 必須有序,須要提早排序
  • 每次查找都須要不斷計算中間位置

二分查找樹
若是一組數據不會或不常變動,那麼他們的位置也基本不變。但是每次查詢都須要從新計算中間位置是一種浪費,而浪費可恥。
咱們能不能把全部中間節點組織起來,每次使用時,直接取中間節點?
請看下圖,找到全部單次二分查找的中間節點,把他們連起來,並用手提起最中間的那個節點,就是一棵二分查找樹。
優勢:二分查找樹就是經過數據結構的方式實現了二分查找算法,經過存儲中間節點的數據,彌補了二分查找每次都要計算中間位置的缺點。

平衡二叉樹:
若是二分查找樹不斷進行修改,好比刪除某些節點,通過一段時間後,最先那個中間節點的數據(根),極可能就不在中間了。
中間位置就像一個天平的支點,若是他不在中間了,那麼整個天平就會失衡,失衡的世界就會坍塌成不三不四的瘸樹,甚至是降維成一個鏈表或者數組。
二分查找算法的關鍵在於有序和中間節點,而二分查找樹的關鍵是中間節點的維護,若是維護的節點已經不在中間了,那麼它就失去了意義。
因此必須保證「二分查找樹」是一個正確的樹,一個根節點在中心的樹,一個左右子樹層級(高度)基本相等(高度相差不超過1)的樹,一個平衡的樹。
平衡二叉樹中最多見的就是紅黑樹:
紅黑樹規定了一系列節點顏色規則,以及對應的左旋和右旋操做來保證顏色規則,從而達到樹的平衡性。
看到這花裏胡哨的顏色以及複雜的規則,讓人第一眼就望而卻步,但全部的這些,也不過是爲了保證二叉樹的平衡性,因爲維持平衡的操做太過麻煩,沒法用一句話簡單歸納,只好用一堆人鬼難分的規則和步驟來實現,只要按着這些步驟就必定能實現二叉樹的平衡。
平衡二叉樹 = 二分查找樹 + 平衡(左右高度相差不超過 1 )
平衡二叉樹並未提升二分查找樹的性能,它只是保正樹不會被二向箔(屢次增刪改)打擊降維成鏈表或不對稱的殘缺樹,永遠維持平衡。
另外,不只僅是二叉樹,其餘種類的樹,也是須要有序和平衡,才能發揮最大的威力。

多叉樹之 B-tree
兩個叉的樹就能折半查詢,理論能夠提升一倍性能,那麼多個叉是否是能提升更多倍性能?
以下圖的 3 階(叉)樹(全部數據僅用於演示,非真實分佈)
每一個節點維護兩個數據,並指向最多 3 個子節點。如圖 3 個子節點的數據分別爲:小於 17, 17 ~ 35 ,大於 35。
假設,從上圖中查找 10 這個數,步驟以下:
  1. 找到根節點,對比 10 與 17 和 35 的大小,發現 10 < 17 在左子節點,也就是第 2 層節點;
  2. 從根節點的指針,找到左子節點,對比 10 與 8 和 12 的大小,發現 8 < 10 < 12,數據在當前節點的中間子節點,也就是第 3 層節點;
  3. 經過上步節點的指針,找到中間子節點(第 3 層節點),對比 10 與 9 和 10 的大小,發現 9 < 10 == 10,所以找到當前節點的第二數即爲結果。
加上忽略的 12 個數據,從 26 個數據中查找一個數字 10,僅僅用了 log3(26)≈ 3 次,而若是用平衡二叉樹,則須要 log2(26)≈5 次,事實證實,多叉樹確實能夠再次提升查找性能。
多叉樹是在二分查找樹的基礎上,增長單個節點的數據存儲數量,同時增長了樹的子節點數,一次計算能夠把查找範圍縮小更多。
優勢:二叉平衡樹的基礎上,使加載一次節點,能夠加載更多路徑數據,同時把查詢範圍縮減到更小。
複雜節點: 至此,咱們列舉的數據都是孤零零的單個數字。試想,你手裏已經有一個數據 10,爲何還要費力吧唧的再從一堆數據中找到這個 10,本身找本身?這不是有病嗎?
單個數字只能活在演示中,現實的世界要複雜的多,咱們來看一個接近真實場景的案例。
現有一個以年齡爲索引的 3 階樹,存儲了一批用戶信息,以下圖:
數字爲用戶的年齡,其它爲與樹排序查找無關的業務數據,像這種索引數據與樹排序查找無關的業務一塊兒維護在節點的平衡多叉(階)樹稱爲 B- 樹( B 樹)。
缺點:業務數據的大小可能遠遠超過了索引數據的大小,每次爲了查找對比計算,須要把數據加載到內存以及 CPU 高速緩存中時,都要把索引數據和無關的業務數據所有查出來。原本一次就能夠把全部索引數據加載進來,如今卻要屢次才能加載完。若是所對比的節點不是所查的數據,那麼這些加載進內存的業務數據就毫無用處,所有拋棄。

磁盤I/O
計算機的功能主要爲:計算、存儲和網絡。而用於計算的數據以及計算後的結果很大一部分都須要存儲起來,以備後續再次使用。向磁盤中存儲和讀取的過程叫磁盤 I/O。磁盤的讀取方式和速度會嚴重影響到整個業務的計算性能。
下面咱們簡單瞭解一下磁盤是如何工做的。
磁盤大概長這個樣子:
磁盤主要由磁盤盤片、傳動手臂、讀寫磁頭和馬達組成。
爲了存儲容量,主軸像穿糖葫蘆同樣把多個磁盤片組成一個陣列。經過馬達驅動主軸轉動以及傳動手臂移動,使讀寫磁頭在磁盤片上讀寫數據。大概以下:
磁盤片由不少半徑不等的同心圓組成,這些圓被稱爲磁道,數據就是寫在這些磁道上。
每一個磁道又劃分紅塊稱爲扇區。
若是磁盤是一記事本,那麼一張磁盤片就是本子的一頁紙,而主軸就是本子的裝訂線;磁道就是紙頁的行,而扇區能夠看做是很寬的列。
若是在磁盤中存儲一首詩,想象中大概這個樣子。
磁盤的讀 I/O 操做,須要找到數據所在的磁盤片,以及對應的磁道和扇區。這些操做相似於從一本書中找到數據所在的頁,行,列。
由於每一個磁盤片都對應一個磁頭,因此性能的關鍵就在於找行和列,即尋道和磁盤旋轉。尋道即經過磁頭找到數據所在的磁道,至關於換行到數據所在行。因爲磁頭只能水平移動,即只能換行尋道,沒法在指定磁道上移動,所以須要磁盤高速旋轉移動到指定扇區,相似寫春聯時,筆不動,紙動。
綜上所述,磁盤的讀寫是經過機械運動來定位數據所在位置,而 cpu 是經過電信號進行數字運算。粗略的認爲,機械查詢數據,與光速處理數據的性能徹底不是在一個量級,總之一句話就是磁盤處理太慢太慢了
雖然磁盤處理數據太慢了,可是它是目前相對廉價且穩定的存儲設備,因此又不能捨棄不用,但大體能夠經過如下方法進行優化。
  • 儘可能減小 I/O 次數,好比可使用緩存;
  • 每次 I/O 儘可能獲取更多的數據;
  • 每次 I/O 儘可能獲取有用的數據,固然相應的也間接減小總 I/O 次數;

多叉樹之 B+tree
作爲數據庫的索引,不管用什麼樣的數據結構維護,這些數據最終都會存儲到磁盤中。
鑑於磁盤 I/O 的性能問題,以及每次 I/O 獲取數據量上限所限,提升索引自己 I/O 的方法最好是,減小 I/O 次數和每次獲取有用的數據。
B-tree 已經大大改進了樹家族的性能,它把多個數據集中存儲在一個節點中,自己就可能減小了 I/O 次數或者尋道次數。
可是仍然有一個致命的缺陷,那就是它的索引數據與業務綁定在一塊,而業務數據的大小頗有可能遠遠超過了索引數據,這會大大減少一次 I/O 有用數據的獲取,間接的增長 I/O 次數去獲取有用的索引數據。
由於業務數據纔是咱們查詢最終的目的,可是它又是在「二分」查找中途過程無用的數據,所以,若是隻把業務數據存儲在最終查詢到的那個節點是否是就能夠了?
理想很豐滿,現實很骨瘦如柴,誰知道哪一個節點就是最終要查詢的節點呢?
B+tree 橫空出世,B+ 樹就是爲了拆分索引數據與業務數據的平衡多叉樹
B+ 樹中,非葉子節點只保存索引數據,葉子節點保存索引數據與業務數據。這樣即保證了葉子節點的簡約乾淨,數據量大大減少,又保證了最終能查到對應的業務數。既提升了單次 I/O 數據的有效性,又減小了 I/O 次數,還實現了業務。
可是,在數據中索引與數據是分離的,不像示例那樣的?
如圖:咱們只須要把真實的業務數據,換成數據所在地址就能夠了,此時,業務數據所在的地址在 B+ 樹中充當業務數據。

總結
  • 數據存儲在磁盤( SSD 跟 CPU 性能也不在一個量級),而磁盤處理數據很慢;
  • 提升磁盤性能主要經過減小 I/O 次數,以及單次 I/O 有效數據量;
  • 索引經過多階(一個節點保存多個數據,指向多個子節點)使樹的結構更矮胖,從而減小 I/O 次數;
  • 索引經過 B+ 樹,把業務數據與索引數據分離,來提升單次 I/O 有效數據量,從而減小 I/O 次數;
  • 索引經過樹數據的有序和「二分查找」(多階樹能夠假設爲多分查找),大大縮小查詢範圍;
  • 索引針對的是單個字段或部分字段,數據量自己比一條記錄的數據量要少的多,這樣即便經過掃描的方式查詢索引也比掃描數據庫表自己快的多;

知識擴展
樹的結構最大的優勢就是查詢性能高,所以全部須要提升查詢性能的均可以考慮樹。
而現實中也確實有這樣的例子,好比:
  • HashMap 中的數據衝突時,鏈表轉化成紅黑樹;
  • 數據庫索引使用的 B+ 樹;
  • 搜索引擎倒排索引使用的字典樹;
以上只是淺嘗輒止、點到爲止的描述了數據庫使用 B+ 樹索引爲何能提升查詢性能緣由及簡單過程。
並無深刻各類數據結構的細節,也未說起其它索引類型和索引的具體存儲格式,目的僅僅是,爲了讓你們對索引有一個感性的認識。
 
感謝閱讀
 

有完整的Java初級,高級對應的學習路線和資料!專一於java開發。分享java基礎、原理性知識、JavaWeb實戰、spring全家桶、設計模式、分佈式及面試資料、開源項目,助力開發者成長!java


歡迎關注微信公衆號:碼邦主

 

原做者:木葉瀟瀟程序員

連接:https://mp.weixin.qq.com/s/KxSlNnXQSaMemYdqyRCMOg面試

來源:小林coding /侵刪算法

相關文章
相關標籤/搜索