[貝聊科技]理解資料庫索引

貝聊科技發表於2018-05-29

作者:陳浩 前端開發部 iOS 開發工程師

在日常開發中,資料庫降低了我們運算元據的門檻,我們只要編寫特定的 SQL,就能對資料做增刪改查操作。在簡化的背後,往往都隱藏著效能優化的福利,資料庫也是如此,我們知道假如沒有索引,查詢資料就會全表掃描,而索引就如書的目錄一般,大大提高了查詢效率。本文將對資料庫索引進行介紹,認識索引的資料結構,同時也介紹索引的其他概念。

索引的資料結構

索引在本質上是為了優化查詢的速度,對於給定的資料,我們可以使用順序查詢,如果資料已經排好序,我們可以使用二分查詢,如果查詢的資料量不大,我們可以構造二叉查詢樹將查詢放在記憶體中,而索引的資料結構是由平衡二叉樹演化而來,在正式介紹索引的資料結構之前,讓我們先來看看二叉查詢樹。

二叉查詢樹

二叉查詢樹要求左子樹的鍵值總是小於根的鍵值,右子樹的鍵值總是大於根的鍵值。

1527218750414

1527217930423

二叉查詢樹的問題是假如單支過長就會大大影響其查詢效率,甚至退化成順序查詢

1527218408243

為了提高二叉樹的查詢效率,需要構造的這棵二叉樹是平衡的——平衡二叉樹要求任何結點的兩個子樹的高度最大差為1。平衡二叉樹通常需要左旋、右旋來達到平衡。

1527219158761

B* 樹

B- 樹

我們先來看看對 B 樹的描述:

  • B 樹的 B 不是表示二叉,而是表示平衡
  • B 樹並不是一顆二叉樹,B 樹是 n 叉的(n > 2)
  • 每個結點有多個關鍵字,關鍵字之間又有指向孩子結點的指標
  • 一個結點內的關鍵字都有序排列
  • 所有葉子結點都在同一層

1527237093708

對於查詢而言,B 樹的查詢類似二叉樹,因為每個結點內的關鍵字都是排序好的 key[1...n],我們可運用二分查詢將查詢關鍵字 k 與 key[i] 比較,從而找出相應區間的子樹。

B 樹查詢的簡化程式碼:

Result BTreeSearch(BTNode *t, KeyType k) {
    BTNode *p = t; *q = NULL; // q 指向 p 的雙親
    int found = 0, i = 0;
    while (p != NULL && found == 0) {
        i = BinarySearch(p, k);
        if (i > 0 && p->key[i] == k) {
            found = 1;
        } else {
            q = p;
            p = p->ptr[i];
        }
    }
    ...
}
複製程式碼

上述程式碼也可以使用遞迴。有了這些基本的認識後,不難發現 B 樹的查詢效率與樹的高度有關,高度越小,查詢的次數就越少。

接下來看看 B 樹的插入和刪除。 對插入而言,如果該結點還有空位置,直接插入,否則,會將結點分成兩部分,中間位置的關鍵字插入到父結點中,如果父結點也不滿足,再往上插,直到這個過程傳到根結點。

1527237417928

插入15

刪除比插入稍微複雜一點,如果刪除一個關鍵字後,結點的關鍵字個數沒有少於它的裝填因子,則直接刪除

1527237872376

刪除8,16

否則分兩種情況:

  • 如果左(右)兄弟結點的關鍵字個數大於裝填因子,則將左(右)結點最大或最小關鍵字上移到父結點,再把父結點中大於或小於上移關鍵字的關鍵字下移到要被刪除的結點中。

1527582084766

刪除15

  • 如果左(右)兄弟結點的關鍵字個數等於裝填因子,這時需要把刪除關鍵字的結點與左(右)結點關鍵字和分割二者關鍵字的雙親結點關鍵字合併成一個結點,如果因此使雙親結點關鍵字個數小於裝填因子,則對雙親結點也做同樣處理,以致可能使整棵樹的高度減少一層。

1527238316921

刪除4後的結果

由上可知,B 樹的插入和刪除都是需要代價的,所以我們對資料庫索引的建立也需要特別謹慎,否則不合理的索引反而降低了效率。

B+ 樹

B+ 樹是 B- 樹的變形,常用於索引結構中,它與 B- 樹的主要差異有:

  • B+ 樹中所有葉子結點包含了全部關鍵字,即非葉子結點的關鍵字也出現在葉子結點中
  • 葉子結點的指標不再指向另一級索引,而是直接指向資料檔案的記錄
  • 分支結點不包含關鍵字對應的儲存地址,只包含指向各個子結點的指標
  • 所有葉子結點連結成一個線性連結串列

1527582980853

B 樹和平衡二叉樹的一個重要區別是結點的大小及其造成的樹的高度不同,B+ 樹的結點大小一般是一個磁碟塊的大小,也就是資料頁的大小,因此 B 樹矮而胖,二叉樹高而瘦。前面已經提到 B 樹的查詢效率和其高度有關,假設當前資料表的資料為 N,每個磁碟塊的資料項的數量是 m,則有 h = ㏒(m+1)N,而 m = 磁碟塊的大小 / 資料項的大小,磁碟塊的大小又是固定的,故資料項的大小越小,樹的高度也就越小。這就是為什麼要求索引欄位儘可能小的原因。同理,將資料不儲存在分支結點,也是為了儘可能多的存放資料項。

B+ 樹索引

B+ 樹索引就是 B+ 樹在資料庫中的實現。B+ 索引在資料庫中有一個特點是高扇出性,因此在資料庫中,B+ 樹的高度一般都在2~4層,這也就是說查詢某一鍵值的行記錄時最多隻需要2到4次 IO。

B+ 樹索引並不能找到一個給定鍵值的具體行。B+樹索引能找到的只是被查詢資料行所在的頁。然後資料庫通過把頁讀入到記憶體,再在記憶體中進行查詢,最後得到要查詢的資料。

聚集索引和輔助索引

B+ 樹索引可分為聚集索引和輔助索引(secondary index),它們的主要區別是聚集索引要求以唯一的 key(一般是主鍵)來構造索引,檔案中記錄的物理儲存順序和索引順序一致,由於實際的資料頁只能按照一棵 B+ 樹進行排序,因此每張表只能擁有一個聚集索引,輔助索引的 key 可以不是唯一的,輔助索引能提高聚集索引以外 key 的查詢效能,這也會增加一定的開銷。

下面表有三個列,分別是 id(主鍵)、name 和 salary,我們來看看聚集索引和輔助索引的原理:

1527560106240

以上是聚集索引

1527560883816

以上是輔助索引

聯合索引

我們已經介紹過了在單個列上使用索引,聯合索引是指對錶上的多個列進行索引,聯合索引的本質也是一棵 B+ 樹,下面看看聯合索引的內部結構:

dff64b61-998e-47be-910d-02b596227f71

對上圖而言,(1,1)、(1,2)、(2,1)、(2,4)、(3,1)、(3,2),資料按(a,b)的順序進行了存放,第一列是升序排序的,第二列是根據第一列排序而排序的。

因此,對於查詢 SELECT*FROM TABLE WHERE a=xxx and b=xxx,顯然是可以使用(a,b)這個聯合索引的。對於單個的a列查詢 SELECT*FROM TABLE WHERE a=xxx,也可以使用這個(a,b)索引。但對於b列的查詢 SELECT*FROM TABLE WHERE b=xxx,則不可以使用這棵B+樹索引。可以發現葉子節點上的b值為1、2、1、4、1、2,顯然不是排序的,因此對於b列的查詢使用不到(a,b)的索引。

聯合索引能在索引到第一個鍵值後對第二個鍵值進行排序。例如,查詢某個使用者的購物情況,並按照時間進行排序,最後取出最近三次的購買記錄,這時使用聯合索引可以避免多一次的排序操作,因為索引到某個使用者 id 後,購買記錄已經是有序的了。

正如前面所介紹的那樣,聯合索引(a,b)其實是根據列a、b進行排序,因此語句 SELECT...FROM TABLE WHERE a=xxx ORDER BY b 可以直接使用聯合索引得到結果。

然而對於聯合索引(a,b,c)來說,語句 SELECT...FROM TABLE WHERE a=xxx AND b=xxx ORDER BY cSELECT...FROM TABLE WHERE a=xxx ORDER BY b 同樣可以直接通過聯合索引得到結果。

但對於語句 SELECT...FROM TABLE WHERE a=xxx ORDER BY c,聯合索引不能直接得到結果,因為 c 是用不到索引的。

這就是索引最左字首匹配的特性。根據該原則,我們建立聯合索引時要考慮好查詢儘可能地用得上索引,這也要求我們儘可能選擇區分度高的列作為索引。

小結

  • 在作為主鍵的列上,強制該列的唯一性和組織表中資料的排列結構。
  • 在經常用在連線的列上,這些列主要是一些外來鍵,可以加快連線的速度。
  • 在經常需要根據範圍進行搜尋的列上建立索引,因為索引已經排序,其指定的範圍是連續的。
  • 在經常需要排序的列上建立索引,因為索引已經排序,這樣查詢可以利用索引的排序,加快排序查詢時間。
  • 在經常使用在where子句中的列上面建立索引,加快條件的判斷速度。

雜湊索引和點陣圖索引

最後再簡單介紹下另外兩種索引結構

雜湊索引

雜湊索引通過雜湊演算法來實現查詢,其衝突解決採用鏈地址法,我們知道雜湊演算法的時間複雜度為 O(1),所以雜湊索引是非常高效的。

因為雜湊索引的記錄不以任何特定方式排序,這也導致雜湊索引無法應用在範圍查詢中。

點陣圖索引

點陣圖索引(bitmap index)是為多個列查詢設計的特殊索引,點陣圖索引適合用於列上的值大量重複出現。

表結構:

ID gender income_level
43123 m L1
65654 f L2
76534 f L1
12343 m L4
65765 f L3

gender 的點陣圖:

m 10010
f 01101

income_level 的點陣圖:

L1 10100
L2 01000
L3 00001
L4 00010
L5 00000

上述表對於只以性別為條件的查詢,點陣圖索引並不能帶來什麼效能的提升。然而對查詢 Select * from t where gender = 'f' and income_level = 'L3',點陣圖索引會執行兩個點陣圖的交操作(邏輯與)。即 gender 的點陣圖 = f(01101) 和 income_level 的點陣圖 = L2(01000) 的交得到點陣圖 01000。顯然對於多個列上大量重複資料項的查詢,點陣圖索引可以提高查詢效率。此外,點陣圖索引還有體積小的優點。

總結

本文是我學習資料庫索引的筆記,僅僅介紹了資料庫的幾種索引的原理,並沒有深入到更加底層的研究,只能對日常開發中如何建立索引、選擇索引起到一定的指示作用,而對於查詢效能的優化還是需要從大量的實踐中總結出經驗。

參考文章

MySQL索引原理及慢查詢優化

MySQL索引背後的資料結構及演算法原理

相關文章