索引是數據庫系統中不可或缺的一個功能,數據庫索引比如是書的目錄,能加快數據庫的查詢速度,其實質是數據庫管理系統中一個排序的數據結構。不一樣的數據庫系統有不一樣的排序結構,目前常見的索引實現類型如 B-Tree index、B+-Tree index、B*-Tree index、Hash index、Bitmap index、Inverted index 等等,各類索引類型都有各自的排序算法。git
雖然索引能夠帶來更高的查詢性能,可是也存在一些缺點,例如:github
Nebula Graph 做爲一個高性能的分佈式圖數據庫,對於屬性值的高性能查詢,一樣也實現了索引功能。本文將對 Nebula Graph的索引功能作一個詳細介紹。算法
開始以前,這裏羅列一些可能會使用到的圖數據庫和 Nebula Graph 專有術語:數據庫
Nebula Graph 是一個圖數據庫系統,查詢場景通常是由一個點出發,找出指定邊類型的相關點的集合,以此類推動行(廣度優先遍歷)N 度查詢。另外一種查詢場景是給定一個屬性值,找出符合這個屬性值的全部的點或邊。在後面這種場景中,須要對屬性值進行高性能的掃描,查出與此屬性值對應的邊或點,以及邊或點上的其它屬性。爲了提升屬性值的查詢效率,在這裏引入了索引的功能。對邊或點的屬性值進行排序,以便快速的定位到某個屬性上。以此避免了全表掃描。bash
能夠看到對圖數據庫 Nebula Graph 的索引要求:數據結構
從架構圖能夠看到,每一個Storage Server 中能夠包含多個 Storage Engine, 每一個 Storage Engine中能夠包含多個Partition, 不一樣的Partition之間經過 Raft 協議進行一致性同步。每一個 Partition 中既包含了 data,也包含了 index,同一個點或邊的 data 和 index 將被存儲到同一個 Partition 中。架構
爲了更好的描述索引的存儲結構,這裏將圖數據庫 Nebula Graph 原始數據的存儲結構一塊兒拿出來分析下。分佈式
Vertex 的索引結構如上表所示,下面來詳細地講述下字段:性能
PartitionId:一個點的數據和索引在邏輯上是存放到同一個分區中的。之因此這麼作的緣由主要有兩點:優化
IndexId:index 的識別碼,經過 indexId 可獲取指定 index 的元數據信息,例如:index 所關聯的 TagId,index 所在列的信息。
Index binary:index 的核心存儲結構,是全部 index 相關列屬性值的字節編碼,詳細結構將在本文的 #Index binary# 章節中講解。
VertexId:點的識別碼,在實際的 data 中,一個點可能會有不一樣 version 的多行數據。可是在 index 中,index 沒有 Version 的概念,index 始終與最新 Version 的 Tag 所對應。
上面講完字段,咱們來簡單地實踐分析一波:
假設 PartitionId 爲 _100,TagId 有 tag_1 和 tag_2,_其中 tag_1 包含三列 :col_t1_一、col_t1_二、col_t1_3,tag_2 包含兩列:col_t2_一、col_t2_2。
如今咱們來建立索引:
能夠看到雖然 tag_1 中有 col_t1_3 這列,可是創建索引的時候並無使用到 col_t1_3,由於在圖數據庫 Nebula Graph 中索引能夠基於 Tag 的一列或多列進行建立。
// VertexId = hash("v_t1_1"),假如爲 50 INSERT VERTEX tag_1(col_t1_1, col_t1_2, col_t1_3), tag_2(col_t2_1, col_t2_2) \ VALUES hash("v_t1_1"):("v_t1_1", "v_t1_2", "v_t1_3", "v_t2_1", "v_t2_2");
從上能夠看到 VertexId 可由 ID 標識對應的數值通過 Hash 獲得,若是標識對應的數值自己已經爲 int64,則無需進行 Hash 或者其餘轉化數值爲 int64 的運算。而此時數據存儲以下:
此時點的 Data 結構
此時點的 Index 結構
說明:index 中 row 和 key 是一個概念,爲索引的惟一標識;
邊的索引結構和點索引結構原理相似,這裏再也不贅述。但有一點須要說明,爲了使索引 key 的惟一性成立,索引的 key 的生成藉助了很多 data 中的元素,例如 VertexId、SrcVertexId、Rank 等,這也是爲何點索引中並無 TagId 字段(邊索引中也沒有 EdgeType 字段),這是由於** IndexId 自己帶有 VertexId 等信息可直接區分具體的 tagId 或 EdgeType**。
Index binary 是 index 的核心字段,在 index binary 中區分定長字段和不定長字段,int、double、bool 爲定長字段,string 則爲不定長字段。因爲** index binary 是將全部 index column 的屬性值編碼鏈接存儲**,爲了精確地定位不定長字段,Nebula Graph 在 index binary 末尾用 int32 記錄了不定長字段的長度。
舉個例子:
咱們如今有一個 index binary 爲 index1,是由 int 類型的索引列1 c一、string 類型的索引列 c2,string 類型的索引列 c3 組成:
index1 (c1:int, c2:string, c3:string)
假如索引列 c一、c二、c3 某一行對應的 property 值分別爲:2三、"abc"、"here",則在 index1 中這些索引列將被存儲爲以下(在示例中爲了便於理解,咱們直接用原值,實際存儲中是原值會通過編碼再存儲):
因此 index1 該 row 對應的 key 則爲 23abchere34;
回到咱們 Index binary 章節開篇說的 index binary 格式中存在 Variable-length field lenght
字段,那麼這個字段的的具體做用是什麼呢?咱們來簡單地舉個例:
如今咱們又有了一個 index binary,咱們給它取名爲 index2,它由 string 類型的索引列1 c一、string 類型的索引列 c2,string 類型的索引列 c3 組成:
index2 (c1:string, c2:string, c3:string)
假設咱們如今 c一、c二、c3 分別有兩組以下的數值:
能夠看到這兩行的 prefix(上圖紅色部分)是相同,都是 "ababab",這時候怎麼區分這兩個 row 的 index binary 的 key 呢?別擔憂,咱們有 Variable-length field lenght
。
若遇到 where c1 == "ab" 這樣的條件查詢語句,在 Variable-length field length 中可直接根據順序讀取出 c1 的長度,再根據這個長度取出 row1 和 row2 中 c1 的值,分別是 "ab" 和 "aba" ,這樣咱們就精準地判斷出只有 row1 中的 "ab" 是符合查詢條件的。
當 Tag / Edge中的一列或多列建立了索引後,一旦涉及到 Tag / Edge 相關的寫操做時,對應的索引必須連同數據一塊兒被修改。下面將對索引的write操做在storage層的處理邏輯進行簡單介紹:
當用戶產生插入點/邊操做時,insertProcessor 首先會判斷所插入的數據是否有存在索引的 Tag 屬性 / Edge 屬性。若是沒有關聯的屬性列索引,則按常規方式生成新 Version,並將數據 put 到 Storage Engine;若是有關聯的屬性列索引,則經過原子操做寫入 Data 和 Index,並判斷當前的 Vertex / Edge 是否有舊的屬性值,若是有,則一併在原子操做中刪除舊屬性值。
當用戶發生 Drop Vertex / Edge 操做時,deleteProcessor 會將 Data 和 Index(若是存在)一併刪除,在刪除的過程當中一樣須要使用原子操做。
Vertex / Edge 的更新操做對於 Index 來講,則是 drop 和 insert 的操做:刪除舊的索引,插入新的索引,爲了保證數據的一致性,一樣須要在原子操做中進行。可是對應普通的 Data 來講,僅僅是 insert 操做,使用最新 Version 的 Data 覆蓋舊 Version 的 data 便可。
在圖數據庫 Nebula Graph 中是用 LOOKUP
語句來處理 index scan 操做的,LOOKUP
語句可經過屬性值做爲判斷條件,查出全部符合條件的點/邊,一樣 LOOKUP
語句支持 WHERE
和 YIELD
子句。
正如根據本文#數據存儲結構#章節所描述那樣,index 中的索引列是按照建立 index 時的列順序決定。
舉個例子,咱們如今有 tag (col1, col2),根據這個 tag 咱們能夠建立不一樣的索引,例如:
咱們能夠對 clo一、col2 創建多個索引,但在 scan index 時,上述四個 index 返回結果存在差別,甚至是徹底不一樣,在實際業務中具體使用哪一個 index,及 index 的最優執行策略,則是經過索引優化器決定。
下面咱們再來根據剛纔 4 個 index 的例子深刻分析一波:
lookup on tag where tag.col1 ==1 # 最優的 index 是 index1 lookup on tag where tag.col2 == 2 # 最優的 index 是index2 lookup on tag where tag.col1 > 1 and tag.col2 == 1 # index3 和 index4 都是有效的 index,而 index1 和 index2 則無效
在上述第三個例子中,index3 和 index4 都是有效 index,但最終必需要從二者中選出來一個做爲 index,根據優化規則,由於 tag.col2 == 1 是一個等價查詢,所以優先使用 tag.col2 會更高效,因此優化器應該選出 index4 爲最優 index。
在這部分咱們就不具體講解某個語句的用途是什麼了,若是你對語句不清楚的話能夠去圖數據庫 Nebula Graph 的官方論壇進行提問:https://discuss.nebula-graph.io/
(user@127.0.0.1:6999) [(none)]> CREATE SPACE my_space(partition_num=3, replica_factor=1); Execution succeeded (Time spent: 15.566/16.602 ms) Thu Feb 20 12:46:38 2020 (user@127.0.0.1:6999) [(none)]> USE my_space; Execution succeeded (Time spent: 7.681/8.303 ms) Thu Feb 20 12:46:51 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG lookup_tag_1(col1 string, col2 string, col3 string); Execution succeeded (Time spent: 12.228/12.931 ms) Thu Feb 20 12:47:05 2020 (user@127.0.0.1:6999) [my_space]> CREATE TAG INDEX t_index_1 ON lookup_tag_1(col1, col2, col3); Execution succeeded (Time spent: 1.639/2.271 ms) Thu Feb 20 12:47:22 2020
(user@127.0.0.1:6999) [my_space]> DROP TAG INDEX t_index_1; Execution succeeded (Time spent: 4.147/5.192 ms) Sat Feb 22 11:30:35 2020
若是你是從較老版本的 Nebula Graph 升級上來,或者用 Spark Writer 批量寫入過程當中(爲了性能)沒有打開索引,那麼這些數據尚未創建過索引,這時可使用 REBUILD INDEX 命令來從新全量創建一次索引。這個過程可能會耗時比較久,在 rebuild index 完成前,客戶端的讀寫速度都會變慢。
REBUILD {TAG | EDGE} INDEX <index_name> [OFFLINE]
須要說明一下,使用 LOOKUP 語句前,請確保已經創建過索引(CREATE INDEX 或 REBUILD INDEX)。
(user@127.0.0.1:6999) [my_space]> INSERT VERTEX lookup_tag_1(col1, col2, col3) VALUES 200:("col1_200", "col2_200", "col3_200"), 201:("col1_201", "col2_201", "col3_201"), 202:("col1_202", "col2_202", "col3_202"); Execution succeeded (Time spent: 18.185/19.267 ms) Thu Feb 20 12:49:44 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200"; ============ | VertexID | ============ | 200 | ------------ Got 1 rows (Time spent: 12.001/12.64 ms) Thu Feb 20 12:49:54 2020 (user@127.0.0.1:6999) [my_space]> LOOKUP ON lookup_tag_1 WHERE lookup_tag_1.col1 == "col1_200" YIELD lookup_tag_1.col1, lookup_tag_1.col2, lookup_tag_1.col3; ======================================================================== | VertexID | lookup_tag_1.col1 | lookup_tag_1.col2 | lookup_tag_1.col3 | ======================================================================== | 200 | col1_200 | col2_200 | col3_200 | ------------------------------------------------------------------------ Got 1 rows (Time spent: 3.679/4.657 ms) Thu Feb 20 12:50:36 2020
索引的介紹就到此爲止了,若是你對圖數據庫 Nebula Graph 的索引有更多的功能要求或者建議反饋,歡迎去 GitHub:https://github.com/vesoft-inc/nebula issue 區向咱們提 issue 或者前往官方論壇:https://discuss.nebula-graph.io/ 的 Feedback
分類下提建議 👏
做者有話說:Hi,我是 bright-starry-sky,是圖數據 Nebula Graph 研發工程師,對數據庫存儲有濃厚的興趣,但願本次的經驗分享能給你們帶來幫助,若有不當之處也但願能幫忙糾正,謝謝~