索引為什麼能提供查詢效能...

小林coding發表於2020-11-27

前言

昨天,有個女孩子問我提高資料庫查詢效能有什麼立竿見影的好方法?

這簡直是一道送分題,我自豪且略帶鄙夷的說,當然是加「索引」了。

她又不緊不慢的問,索引為什麼就能提高查詢效能。

這還用問,索引就像一本書的目錄,用目錄查當然很快。

她失望地搖了搖頭,你說的只是一個類比,可為什麼通過目錄就能提高查詢速度呢

唉,對啊,通過書目可以快速查詢,這只是一個現象,真正原因到底是什麼呢。

那女孩看著詫異且表情僵硬的我,滿意而又意味深長的笑笑:原來你這個男程式設計師也不會,看來我還得靠自己研究了。

哎,熬夜又要憔悴了我這該死的美貌。

來自同行的羞辱,是可忍孰不可忍?!

於是,我踏上了資料庫索引學習的不歸路,原來資料庫索引使用了一種叫 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,僅僅用了 $\log_3 26$ $\approx$ 3 次,而如果用平衡二叉樹,則需要 $\log _2{26}$ $\approx$ 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+ 樹索引為什麼能提高查詢效能原因及簡單過程。

並沒有深入各種資料結構的細節,也未提及其它索引型別和索引的具體儲存格式,目的僅僅是,為了讓大家對索引有一個感性的認識。


好文推薦

鍵入網址到顯示,期間發生了什麼...

涼了!張三同學沒答好「程式間通訊」,被面試官掛了....


哈嘍,我是小林,就愛圖解計算機基礎,如果覺得文章對你有幫助,歡迎分享給你的朋友,也給小林點個「在看」,這對小林非常重要,謝謝你們,給各位小姐姐小哥哥們抱拳了,我們下次見!

相關文章