索引(Index)是幫助數據庫系統高效獲取數據的數據結構,數據庫索引本質上是以增長額外的寫操做與用於維護索引數據結構的存儲空間爲代價的用於提高數據庫中數據檢索效率的數據結構。索引能夠幫助咱們快速地定位到數據而不須要每次搜索的時候都遍歷數據庫中的每一行。典型的索引譬如在內存中維護一個二叉查找樹,每一個節點分別包含索引鍵值和一個指向對應數據記錄物理地址的指針,這樣就能夠運用二叉查找在 O(log2n)的複雜度內獲取到相應數據。github
左側爲數據記錄的物理地址,右側爲查找樹,須要注意的是,邏輯上相鄰的記錄在磁盤上也並非必定物理相鄰的。實際的數據庫應用中咱們每每使用 B+ 樹或者 LSM 來替代二叉查找樹或者紅黑樹來構建索引系統,而且充分利用 虛擬存儲管理 https://url.wx-coder.cn/PeNqS 一節中介紹過的局部性原理、磁盤預讀與頁緩存等概念。算法
值得一提的是,本節並未涵蓋搜索引擎中經常使用的與文本索引相關的技術,譬如倒排索引、TF-IDF 等,若是有興趣能夠參考本篇搜索引擎 https://url.wx-coder.cn/O07eI 一章。數據庫
計算機存儲設備可被粗略分爲內存儲器(Main Memory)與外存儲器(External Memory)兩大類,內存存取速度快,但容量小,價格昂貴,並且不能長期保存數據,在不通電狀況下數據會消失;外存儲器存取速度相對較慢,卻能夠吃持久化存儲。若是進行更加細緻地劃分,每一個計算機系統中的存儲設備都被組織成了一個存儲器層次結構,在這個層次結構中,從上至下,設備變得訪問速度愈來愈慢、容量愈來愈大,而且每字節的造價也愈來愈便宜。緩存
存儲器層次結構的主要思想是一層上的存儲器做爲低一層存儲器的高速緩存。所以,寄存器文件就是 L1 的高速緩存,L1 是 L2 的高速緩存,L2 是 L3 的高速緩存,L3 是主存的高速緩存,而主存又是磁盤的高速緩存。在某些具備分佈式文件系統的網絡系統中,本地磁盤就是存儲在其餘系統中磁盤上的數據的高速緩存。網絡
主存是一個臨時存儲設備,在處理器執行程序時,用來存放程序和程序處理的數據。從物理上來講,主存是由一組動態隨機存取存儲器(DRAM)芯片組成的。從邏輯上來講,存儲器是一個線性的字節數組,每一個字節都有其惟一的地址(即數組索引),這些地址是從零開始的。通常來講,組成程序的每條機器指令都由不一樣數量的字節構成。數據結構
現代 DRAM 的結構和存取原理比較複雜,這裏抽象出一個十分簡單的存取模型來講明 DRAM 的工做原理。從抽象角度看,主存是一系列的存儲單元組成的矩陣,每一個存儲單元存儲固定大小的數據。每一個存儲單元有惟一的地址,現代主存的編址規則比較複雜,這裏將其簡化成一個二維地址:經過一個行地址和一個列地址能夠惟必定位到一個存儲單元。架構
當系統須要讀取主存時,則將地址信號放到地址總線上傳給主存,主存讀到地址信號後,解析信號並定位到指定存儲單元,而後將此存儲單元數據放到數據總線上,供其它部件讀取。寫主存的過程相似,系統將要寫入單元地址和數據分別放在地址總線和數據總線上,主存讀取兩個總線的內容,作相應的寫操做。這裏能夠看出,主存存取的時間僅與存取次數呈線性關係,由於不存在機械操做,兩次存取的數據的「距離」不會對時間有任何影響,例如,先取 A0 再取 A1 和先取 A0 再取 D3 的時間消耗是同樣的。分佈式
寄存器文件在層次結構中位於最頂部,也就是第 0 級或記爲 L0。一個典型的寄存器文件只存儲幾百字節的信息,而主存裏可存放幾十億字節。然而,處理器從寄存器文件中讀數據的速度比從主存中讀取幾乎要快 100 倍。針對這種處理器與主存之間的差別,系統設計者採用了更小、更快的存儲設備,即高速緩存存儲器(簡稱高速緩存),做爲暫時的集結區域,用來存放處理器近期可能會須要的信息。
L1 和 L2 高速緩存是用一種叫作靜態隨機訪問存儲器(SRAM)的硬件技術實現的。比較新的、處理能力更強大的系統甚至有三級高速緩存:L一、L2 和 L3。系統能夠得到一個很大的存儲器,同時訪問速度也很快,緣由是利用了高速緩存的局部性原理,即程序具備訪問局部區域裏的數據和代碼的趨勢。經過讓高速緩存裏存放可能常常訪問的數據的方法,大部分的存儲器操做都能在快速的高速緩存中完成。
磁盤是一種直接存取的存儲設備 (DASD)。它是以存取時間變化不大爲特徵的。能夠直接存取任何字符組,且容量大、速度較其它外存設備更快。磁盤是一個扁平的圓盤(與電唱機的唱片相似),盤面上有許多稱爲磁道的圓圈,數據就記錄在這些磁道上。磁盤能夠是單片的,也能夠是由若干盤片組成的盤組,每一盤片上有兩個面。以下圖中所示的 6 片盤組爲例,除去最頂端和最底端的外側面不存儲數據以外,一共有 10 個面能夠用來保存信息。
當磁盤驅動器執行讀 / 寫功能時。盤片裝在一個主軸上,並繞主軸高速旋轉,當磁道在讀 / 寫頭 ( 又叫磁頭 ) 下經過時,就能夠進行數據的讀 / 寫了。通常磁盤分爲固定頭盤 ( 磁頭固定 ) 和活動頭盤。固定頭盤的每個磁道上都有獨立的磁頭,它是固定不動的,專門負責這一磁道上數據的讀 / 寫。活動頭盤 ( 如上圖 ) 的磁頭是可移動的。每個盤面上只有一個磁頭 ( 磁頭是雙向的,所以正反盤面都能讀寫 )。它能夠從該面的一個磁道移動到另外一個磁道。全部磁頭都裝 在同一個動臂上,所以不一樣盤面上的全部磁頭都是同時移動的 ( 行動整齊劃一 )。當盤片繞主軸旋轉的時候,磁頭與旋轉的盤片造成一個圓柱體。各個盤面上半徑相 同的磁道組成了一個圓柱面,咱們稱爲柱面。所以,柱面的個數也就是盤面上的磁道數。
磁盤上數據必須用一個三維地址惟一標示:柱面號、盤面號、塊號 ( 磁道上的盤塊 )。讀 / 寫磁盤上某一指定數據須要下面 3 個步驟: (1) 首先移動臂根據柱面號使磁頭移動到所須要的柱面上,這一過程被稱爲定位或查找。 (2) 如上圖 11.3 中所示的 6 盤組示意圖中,全部磁頭都定位到了 10 個盤面的 10 條磁道上 ( 磁頭都是雙向的 )。這時根據盤面號來肯定指定盤面上的磁道。 (3) 盤面肯定之後,盤片開始旋轉,將指定塊號的磁道段移動至磁頭下。通過上面三個步驟,指定數據的存儲位置就被找到。這時就能夠開始讀 / 寫操做了。訪問某一具體信息,由 3 部分時間組成:
磁盤讀取數據是以盤塊(block)爲基本單位的。位於同一盤塊中的全部數據都能被一次性所有讀取出來。而磁盤 IO 代價主要花費在查找時間 Ts 上。所以咱們應該儘可能將相關信息存放在同一盤塊,同一磁道中。或者至少放在同一柱面或相鄰柱面上,以求在讀/寫信息時儘可能減小磁頭來回移動的次數,避免過多的查找時間Ts
。因此,在大規模數據存儲方面,大量數據存儲在外存磁盤中,而在外存磁盤中讀取 / 寫入塊 (block) 中某數據時,首先須要定位到磁盤中的某塊,如何有效地查找磁盤中的數據,須要一種合理高效的外存數據結構。
哈希索引便是基於哈希技術,如上圖所示,咱們將一系列的最終的鍵值經過哈希函數轉化爲存儲實際數據桶的地址數值。值自己存儲的地址就是基於哈希函數的計算結果,而搜索的過程就是利用哈希函數從元數據中推導出桶的地址。
添加新值的流程,首先會根據哈希函數計算出存儲數據的地址,若是該地址已經被佔用,則添加新桶並從新計算哈希函數。
更新值的流程則是先搜索到目標值的地址,而後對該內存地址應用所需的操做。
哈希索引會在進行相等性測試(等或者不等)時候具備很是高的性能,可是在進行比較查詢、Order By 等更爲複雜的場景下就無能爲力。
在數據結構與算法/查找樹 https://url.wx-coder.cn/9PnzG 一節中咱們介紹了 B-Tree 的基本概念與實現,這裏咱們繼續來分析下爲什麼 B-Tree 相較於紅黑樹等二叉查找樹會更適合於做爲數據庫索引的實現。通常來講,索引自己也很大,不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤 I/O 消耗,相對於內存存取,I/O 存取的消耗要高几個數量級,因此評價一個數據結構做爲索引的優劣最重要的指標就是在查找過程當中磁盤 I/O 操做次數的漸進複雜度。換句話說,索引的結構組織要儘可能減小查找過程當中磁盤 I/O 的存取次數。
根據 B-Tree 的定義,可知檢索一次最多須要訪問 h 個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲等於一個頁,這樣每一個節點只須要一次 I/O 就能夠徹底載入。每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁裏,加之計算機存儲分配都是按頁對齊的,就實現了一個節點只需一次 I/O。而檢索的時候,一次檢索最多須要 h-1 次 I/O(根節點常駐內存),其漸進複雜度爲 ,實際應用中,出度 d 是很是大的數字,一般超過 100,所以 h 很是小(一般不超過 3)。而紅黑樹這種結構,h 明顯要深的多。因爲邏輯上很近的節點(父子)物理上可能很遠,沒法利用局部性,因此紅黑樹的 I/O 漸進複雜度也爲 O(h),效率明顯比 B-Tree 差不少。
B+Tree 是 的變種,有着比 B-Tree 更高的查詢性能,其相較於 B-Tree 有了以下的變化:
有 m 個子樹的節點包含有 m 個元素(B-Tree 中是 m-1)。
根節點和分支節點中不保存數據,只用於索引,全部數據都保存在葉子節點中。
全部分支節點和根節點都同時存在於子節點中,在子節點元素中是最大或者最小的元素。
葉子節點會包含全部的關鍵字,以及指向數據記錄的指針,而且葉子節點自己是根據關鍵字的大小從小到大順序連接。
通常在數據庫系統或文件系統中使用的 B+Tree 結構都在經典 B+Tree 的基礎上進行了優化,增長了順序訪問指針:
如上圖所示,在 B+Tree 的每一個葉子節點增長一個指向相鄰葉子節點的指針,就造成了帶有順序訪問指針的 B+Tree。作這個優化的目的是爲了提升區間訪問的性能,例以下圖中若是要查詢 key 爲從 3 到 8 的全部數據記錄,當找到 3 後,只需順着節點和指針順序遍歷就能夠一次性訪問到全部數據節點,極大提到了區間查詢效率。
B-Tree 索引能夠很好地用於單行、範圍或者前綴掃描,他們只有在查找使用了索引的最左前綴(Leftmost Prefix)的時候纔有用。不過 B-Tree 索引存在一些限制:
所以 B-Tree 的列順序很是重要,上述使用規則都和列順序有關。對於實際的應用,通常要根據具體的需求,建立不一樣列和不一樣列順序的索引。假設有索引 Index(A,B,C):
# 使用索引
A>5 AND A<10 - 最左前綴匹配
A=5 AND B>6 - 最左前綴匹配
A=5 AND B=6 AND C=7 - 全列匹配
A=5 AND B IN (2,3) AND C>5 - 最左前綴匹配,填坑
# 不能使用索引
B>5 - 沒有包含最左前綴
B=6 AND C=7 - 沒有包含最左前綴
# 使用部分索引
A>5 AND B=2 - 使用索引 A 列
A=5 AND B>6 AND C=2 - 使用索引的 A 和 B 列
複製代碼
使用索引對結果進行排序,須要索引的順序和 ORDER BY 子句中的順序一致,而且全部列的升降序一致(ASC/DESC)。若是查詢鏈接了多個表,只有在 ORDER BY 的列引用的是第一個表才能夠(須要按序 JOIN)。
# 使用索引排序
ORDER BY A - 最左前綴匹配
WHERE A=5 ORDER BY B,C - 最左前綴匹配
WHERE A=5 ORDER BY B DESC - 最左前綴匹配
WHERE A>5 ORDER BY A,B - 最左前綴匹配
# 不能使用索引排序
WHERE A=5 ORDER BY B DESC,C ASC - 升降序不一致
WHERE A=5 ORDER BY B,D - D 不在索引中
WHERE A=5 ORDER BY C - 沒有包含最左前綴
WHERE A>5 ORDER BY B,C - 第一列是範圍條件,沒法使用 BC 排序
WHERE A=5 AND B IN(1, 2) ORDER BY C - B 也是範圍條件,沒法用 C 排序
複製代碼
B-Tree 這種數據庫索引方式是傳統關係型數據庫中主要的索引構建方式,然而 BTree 一般會存在寫操做吞吐量上的瓶頸,其須要大量的磁盤隨機 IO,很顯然,大量的磁盤隨機 IO 會嚴重影響索引創建的速度。對於那些索引數據大的狀況(例如,兩個列的聯合索引),插入速度是對性能影響的重要指標,而讀取相對來講就比較少。譬如在一個無緩存的狀況下,B-Tree 首先須要進行一次磁盤讀寫將磁盤頁讀取到內存中,而後進行修改,最後再進行一次 IO 寫回到磁盤中。
LSM Tree 則採起讀寫分離的策略,會優先保證寫操做的性能;其數據首先存儲內存中,然後須要按期 Flush 到硬盤上。LSM-Tree 經過內存插入與磁盤的順序寫,來達到最優的寫性能,由於這會大大下降磁盤的尋道次數,一次磁盤 IO 能夠寫入多個索引塊。HBase, Cassandra, RockDB, LevelDB, SQLite 等都是基於 LSM Tree 來構建索引的數據庫;LSM Tree 的樹節點能夠分爲兩種,保存在內存中的稱之爲 MemTable, 保存在磁盤上的稱之爲 SSTable。
LSM-tree 的主要思想是劃分不一樣等級的樹。以兩級樹爲例,能夠想象一份索引數據由兩個樹組成,一棵樹存在於內存,一棵樹存在於磁盤。內存中的樹能夠能夠是 AVL Tree 等結構;由於數據大小是不一樣的,不必犧牲 CPU 來達到最小的樹高度。而存在於磁盤的樹是一棵 B-Tree。
數據首先會插入到內存中的樹。當內存中的樹中的數據超過必定閾值時,會進行合併操做。合併操做會從左至右遍歷內存中的樹的葉子節點與磁盤中的樹的葉子節點進行合併,當被合併的數據量達到磁盤的存儲頁的大小時,會將合併後的數據持久化到磁盤,同時更新父親節點對葉子節點的指針。
以前存在於磁盤的葉子節點被合併後,舊的數據並不會被刪除,這些數據會拷貝一份和內存中的數據一塊兒順序寫到磁盤。這會操做一些空間的浪費,可是,LSM-Tree 提供了一些機制來回收這些空間。磁盤中的樹的非葉子節點數據也被緩存在內存中。數據查找會首先查找內存中樹,若是沒有查到結果,會轉而查找磁盤中的樹。有一個很顯然的問題是,若是數據量過於龐大,磁盤中的樹相應地也會很大,致使的後果是合併的速度會變慢。一個解決方法是創建各個層次的樹,低層次的樹都比 上一層次的樹數據集大。假設內存中的樹爲 c0, 磁盤中的樹按照層次一次爲 c1, c2, c3, ... ck-1, ck
。合併的順序是 (c0, c1), (c1, c2)...(ck-1, ck)
。
歡迎關注某熊的技術之路公衆號或某熊的技術之路指北,讓咱們一塊兒前行。