全方位講解 Nebula Graph 索引原理和使用

語言: CN / TW / HK

本文首發於 Nebula Graph Community 公眾號

全方位講解 Nebula Graph 索引原理和使用

index not found?找不到索引?為什麼我要建立 Nebula Graph 索引?什麼時候要用到 Nebula Graph 原生索引?針對社群常見問題,本文旨在一文帶大家搞清索引使用問題。

Nebula Graph 的索引其實和傳統的關係型資料庫中的索引很像,但是又有一些容易讓人疑惑的區別。剛開始瞭解 Nebula 的同學會疑惑:

  • 不清楚 Nebula Graph 圖資料庫中的索引到的是什麼概念;
  • 什麼時候應該使用 Nebula Graph 索引;
  • Nebula Graph 索引會影響寫入效能嗎?影響程度如何?

在這篇文章裡,我們就把這些問題一一解決,方便大家更好地使用 Nebula Graph。

到底 Nebula Graph 索引是什麼

簡單來說,Nebula Graph 索引是用來且只用來針對純屬性條件出發查詢場景的功能,它具有以下特性:

  • 圖遊走(walk)查詢中的屬性條件過濾不需要它
  • 純屬性條件出發查詢(注:非取樣情況)必須建立索引

純屬性條件出發查詢

我們知道在傳統關係型資料庫中,索引是對錶資料的一個或多個針對特定重排序的副本,它用來加速特定列過濾條件的讀查詢並帶來了額外的資料寫入。簡單來說,索引能起到加速的作用,但查詢使用索引並非是必要的。

在 Nebula Graph 圖資料庫裡,索引則是對點、邊特定屬性資料重排序的副本,用來提供純屬性條件出發查詢

以如下邊的查詢為例,該語句實現了從指定點邊屬性條件,而非點的 ID 出發的方式去獲取圖資料:

#### 必須 Nebula Graph 索引存在的查詢

# query 0 純屬性條件出發查詢
LOOKUP ON tag1 WHERE col1 > 1 AND col2 == "foo" \
    YIELD tag1.col1 as col1, tag1.col3 as col3;

# query 1 純屬性條件出發查詢
MATCH (v:player { name: 'Tim Duncan' })-->(v2:player) \
        RETURN v2.player.name AS Name;

上邊這兩個純屬性條件出發查詢就是字面意思的”根據指定的屬性條件獲取點或者邊本身“ ,反面的例子則是給定了點的 ID。參考以下例子:

#### 不基於索引的查詢

# query 2, 從給定的點做的遊走查詢 vertex VID: "player100"

GO FROM "player100" OVER follow REVERSELY \
        YIELD src(edge) AS id | \
    GO FROM $-.id OVER serve \
        WHERE properties($^).age > 20 \
        YIELD properties($^).name AS FriendOf, properties($$).name AS Team;

# query 3, 從給定的點做的遊走查詢 vertex VID: "player101" 或者 "player102"

MATCH (v:player { name: 'Tim Duncan' })--(v2) \
        WHERE id(v2) IN ["player101", "player102"] \
        RETURN v2.player.name AS Name;

我們仔細看前邊的 query 1query 3,儘管語句中條件都有針對 tag 為 player 的過濾條件 { name: 'Tim Duncan' },但一個需要依賴索引實現,一個不需要索引。具體的原因在這裡 :

  • query 3之中不需要索引,因為:
    • 它會繞過 (v:player { name: 'Tim Duncan' }) 這種未給定 VID 的起點,從 v2 這樣給定了 VID ["player101", "player102"] 的起點向外擴充套件,下一步再通過 GetNeighbors() 獲得邊的另一端的點,然後 GetVertices() 得到下一跳的 v,根據 v.player.name 過濾掉不要的資料;
  • query 1 則不同,它因為沒有任何給定的起點 VID:
    • 只能從屬性條件 { name: 'Tim Duncan' } 入手,在按照 name 排序的索引資料中先找到符合的點:IndexScan() 得到 v
    • 然後再從 v 做 GetNeighbors() 獲得邊的另一端 的 v2 ,在通過 GetVertices() 去獲得下一跳 v2 中的資料;

其實,這裡的關鍵就是在於是查詢是否存在給定的頂點 ID(Vertex ID),下邊兩個查詢的執行計劃裡更清晰地比較了他們的區別:

query-based-on-index 圖注:query 1 的執行計劃(需要索引);

query-requires-no-index

圖注:query 3 的執行計劃(不需要索引);

為什麼純屬性條件出發查詢裡必須要索引呢?

因為 Nebula Graph 在儲存資料的時候,它的結構是面向分散式與關聯關係設計的,類似表結構資料庫中無索引的全掃描條件搜尋實際上更加昂貴,所以設計上被有意禁止了。

但,如果你不追求全部資料,只要取樣一部分,3.0 裡之後是支援不強制索引 LIMIT <n> 的情況的,如下查詢(有 LIMIT)不需要索引:

MATCH (v:player { name: 'Tim Duncan' })-->(v2:player) \
    RETURN v2.player.name AS Name LIMIT 3;

為什麼只有純屬性條件出發查詢需要索引

在這裡,我們比較一下正常的圖查詢 graph-queries 和純屬性條件出發查詢 pure-prop-condition queries 實現方式:

  • graph-queries,如 query 2query 3 是沿著邊一路找到特定路徑條件的擴充套件遊走;
  • pure-prop-condition queries,如 query 0query 1 是隻通過特定屬性條件(或者是無限制條件)找到滿足的點、邊;

而在 Nebula Graph 裡,graph-queries 在擴充套件的時候,圖的原始資料已經按照 VID(點和邊都是)排序過了,或者說在資料裡已經索引過了,這個排序帶來連續儲存(物理上鄰接)使得擴充套件遊走本身就是優化過能快速返回結果。

總結:索引是什麼,索引不是什麼?

索引是什麼?

  • Nebula Graph 索引是為了從給定屬性條件查點、邊的一份屬性資料的排序,它用寫入的代價使得這種讀查詢模式成為可能。

索引不是什麼?

  • Nebula Graph 索引不是用來加速一般圖查詢的:從一個點開始向外拓展的查詢(即使是過濾屬性條件的)不會依賴原生索引,因為 Nebula 資料自身的儲存就是面向這種查詢優化、排序的。

一些 Nebula Graph 索引的設計細節

為了更好理解索引的限制、代價、能力,咱們來解釋更多他的細節

  • Nebula Graph 索引是在本地(不是分開、中心化)和點資料被一起儲存、分片的。
  • 它只支援左匹配
    • 因為底層是 RocksDB Prefix Scan;
  • 效能代價:
    • 寫入時候的路徑:不只是多一分資料,為了保證一致性,還有昂貴的讀操作;
    • 讀路徑:基於規則的優化選擇索引,fan-out 到所有 StorageD;

這些資訊可在我的個人網站的#手繪圖和影片#(連結:https://www.siwei.io/sketch-notes/)裡可以看到,參考下圖:

因為左匹配的設計,在複雜查詢場景,比如:針對純屬性條件出發查詢裡涉及到通配、REGEXP,Nebula Graph 提供了全文索引的功能,它是利用 Raft Listener 去非同步將資料寫到外部 Elasticsearch 叢集之中,並在查詢的時候去查 ES 去做到的,具體全文索引使用參見文件:https://docs.nebula-graph.com.cn/3.0.0/4.deployment-and-installation/6.deploy-text-based-index/2.deploy-es/

在這個手繪圖中,我們還可以看出

  • Write path
    • 寫入索引資料是同步操作的;
  • Read path
    • 這部分畫了一個 RBO 的例子,查詢裡的規則假設 col2 相等匹配排在左邊的情況下,效能優於 col1 的大小比較匹配,所以選擇了第二個索引;
    • 選好了索引之後,掃描索引的請求被 fan-out 到儲存節點上,這其中有些過濾條件比如 TopN 是可以下推的;

結論:

  • 因為寫入的代價,只有必須用索引的時候採用,如果取樣查詢能滿足讀的要求,可以不建立索引而用 LIMIT <n>。
  • 索引有左匹配的限制
    • 符合查詢的順序要仔細設計
    • 有時候需要使用全文索引 full-text index

索引的使用

具體要參考 Nebula 官方的索引文件:https://docs.nebula-graph.io/3.0.0/3.ngql-guide/14.native-index-statements/ 一些要點是:

第一點,在 Tag 或者 EdgeType 上針對想要被條件反查點邊的屬性建立索引,使用 CREATE INDEX 語句;

第二點,建立索引之後的索引部分資料會同步寫入,但是如果建立索引之前已經有的點邊資料對應的索引是需要明確指定去建立的,這是一個非同步的 job,需要執行語句 REBUILD INDEX

第三點,觸發了非同步的 REBUILD INDEX 之後,可用語句 SHOW INDEX STATUS 查詢狀態:

第四點,利用到索引的查詢可以是 LOOKUP,並且常常可以藉助管道符在此之上做拓展查詢,參考下面例子:

LOOKUP ON player \
    WHERE player.name == "Kobe Bryant"\
    YIELD id(vertex) AS VertexID, properties(vertex).name AS name |\
    GO FROM $-.VertexID OVER serve \
    YIELD $-.name, properties(edge).start_year, properties(edge).end_year, properties($$).name;

也可以是 MATCH,這裡邊 v 是通過索引得到的,而 v2 則是在資料(非索引)部分拓展查詢獲得的。

MATCH (v:player{name:"Tim Duncan"})--(v2:player) \
    RETURN v2.player.name AS Name;

第五點,複合索引的能力與限制。理解原生索引的匹配是左匹配能讓我們知道對於超過一個屬性的索引:複合索引,並且能幫助我們理解它的能力有限制,這裡說幾個結論:

  • 我們建立針對多個屬性的複合索引是順序有關的
    • 比如,我們建立一個雙屬性複合索引 index_a: (isRisky: bool, age: int),和 index_b: (age: int, isRisky: bool) 在根據 WHERE n.user.isRisky == true AND n.user.age > 18 篩選條件進行查詢時,index_a 因為左匹配一個相等的短欄位,顯然效率更高。
  • 只有複合左匹配的被複合索引的屬性真子集的過濾條件才能被只支援
    • 比如,index_a: (isRisky: bool, age: int),和 index_b: (age: int, isRisky: bool) 在查詢 WHERE n.user.age > 18 這個語句時,只有 index_b 複合最左匹配,能滿足這個查詢。
  • 針對一些從屬性作為查詢的起點,找點、邊的情況,原生索引是不能滿足全文搜尋的匹配場景的。這時候,我們應該考慮使用 Nebula 全文索引,它是 Nebula 社群支援的開箱即用的外接 Elasticsearch,通過配置,建立了全文索引的資料會通過 Raft listener 非同步更新到 Elastic 叢集中,全文索引的查詢入口也是 LOOKUP,詳細的資訊請參考文件:https://docs.nebula-graph.com.cn/3.0.1/4.deployment-and-installation/6.deploy-text-based-index/2.deploy-es/

回顧

  • Nebula Graph 索引在只提供屬性條件情況下通過對屬性的排序副本掃描查點、邊;
  • Nebula Graph 索引不是用來圖拓展查詢的;
  • Nebula Graph 索引是左匹配,不是用來做模糊全文搜尋的;
  • Nebula Graph 索引在寫入時候有效能代價;
  • 記得如果建立 Nebula Graph 索引之前已經有相應點邊上的資料,要重建索引;

Happy Graphing!


交流圖資料庫技術?加入 Nebula 交流群請先填寫下你的 Nebula 名片,Nebula 小助手會拉你進群~~

關注公眾號