摘要
數據庫索引是數據庫中最重要的組成部分,而索引的數據結構設計對數據庫的性能有重要的影響。本文嘗試選取幾種典型的索引數據結構,總結分析,以窺數據庫索引之全貌。git
B+Tree
B+Tree 是一種樹數據結構,是一個n叉排序樹,每一個節點一般有多個孩子,一棵B+Tree包含根節點、內部節點和葉子節點。根節點多是一個葉子節點,也多是一個包含兩個或兩個以上孩子節點的節點。github
B+Tree 幾乎是數據庫默認的索引實現,其細節以下:spring
維基百科在 B+ 樹中的節點一般被表示爲一組有序的元素和子指針。若是此B+樹的序數(order)是m ,則除了根以外的每一個節點都包含最少⌊m/2⌋⌊m/2⌋⌊m/2⌋⌊m/2⌋ 個元素最多 m-1 個元素,對於任意的節點有最多 m 個子指針。對於全部內部節點,子指針的數目老是比元素的數目多一個。由於全部葉子都在相同的高度上,節點一般不包含肯定它們是葉子仍是內部節點的方式。
每一個內部節點的元素充當分開它的子樹的分離值。例如,若是內部節點有三個子節點(或子樹)則它必須有兩個分離值或元素 a1 和 a2。在最左子樹中全部的值都小於等於 a1,在中間子樹中全部的值都在 a1 和 a2 之間((a1,a2]),而在最右子樹中全部的值都大於 a2。數據庫
B+Tree 有以下性質:數組
- 查詢時間複雜度爲 O(logmn)O(logmn)
- 插入時間複雜度 O(logmn)O(logmn)
- 刪除時間複雜度 O(logmn)O(logmn)
- 搜索一個範圍的鍵(k 個鍵)時間複雜度爲 O(logmn+k)O(logmn+k)
B+ Tree 的多線程同步
- 搜索:從根節點開始,獲取子節點的讀閂,而後釋放父節點的讀閂;重複這個過程,直到找到目標節點位置。
- 插入/刪除:從根節點開始,獲取子節點的寫閂;重複這個過程,直到找到目標節點位置;若是子節點是安全的,插入/刪除不會引發樹結構的變化即父節點不須要調整,可釋放全部祖先寫閂;樂觀的插入/刪除是先走搜索得到目標節點的讀閂,若是目標節點並不安全,則迴歸上述從根節點得到寫閂的過程。
Skip List(跳錶)
Skip List是一種隨機化的數據結構,基於並聯的鏈表,其效率可比擬於二叉查找樹(對於大多數操做須要O(log n)平均時間)。基本上,跳躍列表是對有序的鏈表增長上附加的前進連接,增長是以隨機化的方式進行的,因此在列表中的查找能夠快速的跳過部分列表(所以得名)。全部操做都以對數隨機化的時間進行。Skip List能夠很好解決有序鏈表查找特定值的困難。緩存
一個跳錶,應該具備如下特徵:安全
- 一個跳錶應該有幾個層(level)組成;
- 跳錶的第一層包含全部的元素;
- 每一層都是一個有序的鏈表;
- 若是元素x出如今第i層,則全部比i小的層都包含x;
- 第i層的元素經過一個down指針指向下一層擁有相同值的元素;
- 在每一層中,-1和1兩個元素都出現(分別表示INT_MIN和INT_MAX);
- Top指針指向最高層的第一個元素。
相對於 B+Tree,Skip List 有以下優點:數據結構
- B+ Tree 的插入刪除操做有可能會引發樹結構的變化,須要重新平衡;與之相對的,跳錶插入要簡單的多,更加簡單高效。
- B+ Tree 的實現諸如保持樹平衡很是複雜;與之相對的,跳錶並無很是複雜的邏輯,實現相對更加簡單。
- 取下一個元素能夠再常數時間內,相對於 B+ Tree 的對數時間。
- 由於鏈表很是簡單,能夠很容易的修改跳錶結構,以更好地支持諸如範圍索引之類的操做。
- 鏈表結構使得多線程修改能夠僅用 CAS 保證原子性,從而避免重量級的同步機制。
- 鏈表的持久化更加簡單。
跳錶橫向看來是有不少鏈表組成,然而指針跳轉對於 CPU 緩存 來說很是不友好,能夠用縱向數組來實現跳錶以增長 CPU 緩存。
併發
Bw-Tree
Hekaton 是微軟 SQLServer 專門針對 OLTP 應用場景進行優化的數據庫引擎,其索引實現基於 Bw-Tree。Bw-Tree 是一種無需使用任何閂同步的 B+Tree,其主要設計思想以下:
- Mapping Table(映射表) 映射表存儲內存頁的ID與其對應的物理內存地址,使得線程能夠經過訪問映射表找到須要方位的內存地址,映射表的更新經過CAS操做。
- 不直接修改節點,任何的更新操做都會生成新的數據並經過指針指向被更新節點;新生成的數據所致使的元數據的修改,好比修改映射表都經過 CAS 完成。
- 垃圾回收,Bw-Tree 經過不斷新增數據的方式避免直接修改樹節點,在樹不斷更新的過程當中,不可避免的會產生不少垃圾,所以 Bw-Tree 實現了基於 Epoch 的垃圾回收機制:當一個線程想保護一個它正在使用可是將會被回收的對象,例如檢索的時候,訪問了一個內存頁,就把當前線程加入 Epoch,當這個線程完成檢索頁面的操做後,就會退出 Epoch。一般一個線程在一個epoch的時間間隔內完成一次操做,例如檢索。在線程成功加入 Epoch 的時候,可能會看到將要被釋放的老版本的對象,但不可能看到已經在前一個 Epoch 中釋放的對象,由於其在當前 Epoch 中的操做並不依賴上一個 Epoch 中的數據。所以,一旦全部的線程成功加入Epoch 並完成操做而後退出這個Epoch,回收該 Epoch 中的全部對象是安全的。
因爲維護了映射表,和新增數據鏈,所以樹結構調整相對複雜,不只僅要調整樹,切要保證樹結構和映射表之間的關係。具體操做可參考此篇文章。
儘管實現很是複雜,Bw-Tree 做爲無鎖的數據庫索引樹,有以下優點:
- 無閂: 實現無鎖數據結構十分困難,Bw-Tree 在多線程場景下沒有引入任何的閂,只使用 CAS 指令保證線程同步,所以多核的擴展性優於普通用閂同步的B+Tree。
- CPU 緩存: 因爲不直接修改節點而是追加修改補丁,所以 CPU 緩存不會應爲更新數據而失效,所以能夠顯著提升 CPU 緩存命中率。微軟論文中的數據代表,90% 的讀操做數據來自 CPU L1/L2 緩存。
Adaptive Radix Tree(自適應基數/前綴樹,ART)
Radix Tree (基數樹)是一種常見的前綴樹,Linux Kernel 文件系統就用到了該數據結構:
]Hyper 數據庫中實現了 Adaptive Radix Tree (自適應基數/前綴樹,ART)做爲其索引。基數樹的每一個節點能夠存儲任意長度的鍵切片,好比 Linux Kernel 中的基數樹每一個節點存儲 6位的鍵切片;然而數據庫索引不少場景下會被頻繁修改,每一個節點固定長度的鍵切片會形成時間(切片過長)和空間上(切片太短)的浪費,所以,Hyper 實現了自適應的基數樹,也就是節點根據長度的不一樣分紅若干種,隨着數據的變化而自行調整。
其主要特色有:
- 樹的高度僅取決於鍵長度。
- 更新和刪除不涉及到樹結構的調整,不須要平衡操做。
- 到達葉子節點的路徑就是鍵。
- 時間複雜度取決於鍵的長度,而跟數據量無關,若是數據的增長遠遠超過鍵長度的增長,那麼使用 ART 將會在性能上帶來很是大的收益。
講述ART同步的論文中提供了描述了兩種ART的同步機制:
- 樂觀鎖:
- 讀不阻塞寫
- 寫操做在得到對應的節點閂以後,更新版本信息
- 讀操做在讀下一個結點前,檢查版本信息是否發生改變
- 樂觀讀悲觀寫
- 全部的節點都包含一個互斥鎖,當某一個讀操做得到此互斥鎖以後,阻塞其餘寫操做
- 讀操做不用獲取任何的鎖或者閂,也不用檢查版本信息
- 寫操做保證同一個節點讀操做的數據一致性,即寫操做使用原子指令進行寫入
Masstree
2012年發表的論文 Cache craftiness for fast multicore key-value storage 提出了 Masstree,其特色以下:
- 能夠理解爲B+ Tree 和 Radix Tree 的混合體,即將鍵切分紅多個部分,每一個部分爲一個節點;每一個節點內部又是一個 B+ Tree,兼顧空間和性能。
- Masstree將變長鍵劃分紅多個固長部分,每一個固長部分能夠經過int類型表示,而不是char類型。因爲處理器處理int類型比較操做的速度遠遠快於char數組的比較,所以Masstree經過int類型的比較進一步加速了查找過程。固定長度能夠設置爲 CPU 緩存行長度,以增長 CPU 緩存效率。
- 每一個節點是一個 B+ Tree,所以 CPU 在查詢的時候能夠將節點所表明的B+ Tree 加載到 CPU 緩存中,以增長 CPU 緩存命中率。
- 其併發控制用到了Read-Copy-Update(RCU)。讀不因任何數據更新而阻塞,但更新數據的時候,須要先複製一份副本,在副本上完成修改,再一次性地替換舊數據。所以讀不會形成 CPU 緩存無效。
性能對比
上述幾種索引數據結構性能對比以下: