本文首發於 Nebula Graph 公衆號 NebulaGraphCommunity,Follow 看大廠圖數據庫技術實踐。git
在先前的 Query Engine 源碼解析中,咱們介紹了 2.0 中 Query Engine 和 1.0 的主要變化和大致的結構:github
你們能夠大概瞭解到用戶經過客戶端發送一條查詢語句,Query Engine 是如何解析語句、把語句構建爲抽象語法樹,在抽象語法樹進行校驗、生成執行計劃的過程。本文會經過 2.0 中新增的子圖算法模塊繼續講解 Query Engine 背後所作的內容,並着重介紹執行計劃生成的過程,以便增強你對源碼更好地理解。算法
子圖是指節點集合和邊集合分別是某一圖的節點集的子集和邊集的子集的圖。直觀地理解,就是從用戶指定的起點開始出發沿着指定的邊一步步拓展,直到達到用戶所設定的步數爲止,而後返回在拓展過程當中遇到的全部點集和邊集。shell
GET SUBGRAPH [<step_count> STEPS] FROM {<vid>, <vid>...} [IN <edge_type>, <edge_type>...]
[OUT <edge_type>, <edge_type>...] [BOTH <edge_type>, <edge_type>...]
複製代碼
step_count
跳的子圖。必須是非負整數。默認值爲 1IN
、OUT
和 BOTH
來指定起始點上該邊類型的方向。默認爲 BOTH
當 Query Engine 接收到 GET SUBGRAPH
命令後,Parser 模塊(由 flex 和 bison 實現)會根據已經寫好的規則(parser.yy
中 get_subgraph_sentence
規則)把所須要的內容從查詢語句中提取出來,生成一個抽象語法樹,以下所示:數據庫
而後進入 Validate 階段,此時對生成的抽象語法樹進行校驗,目的是爲了驗證用戶的輸入是否合法(參考 Query Engine 的文章),當校驗經過後,會把語法樹中的內容提取出來,生成一個執行計劃。 那麼這個執行計劃是如何生成的呢?對同一功能不一樣的數據庫廠商可能會生成不一樣的執行計劃,可是原理都是相同的。那就是要看自身的算子有哪些和查詢層和存儲層是如何進行交互的。由於咱們的每一條查詢語句到最後都是要從存儲層取數據的。在 Nebula Graph 中 Query Engine 和存儲層是經過 RPC 方式(fbthrift)進行交互的(接口定義在 common 倉中的 interface 目錄下)。這裏有兩個很是關鍵的接口 getNeighbors 和 getProps 須要瞭解一下。markdown
getNeighbors 其中 fbthrift 的定義格式以下:架構
struct GetNeighborsRequest {
1: common.GraphSpaceID space_id,
2: list<binary> column_names,
3: map<common.PartitionID, list<common.Row>>
(cpp.template = "std::unordered_map") parts,
4: TraverseSpec traverse_spec
}
複製代碼
該結構中每一個變量的詳細定義能夠參考 github.com/vesoft-inc/…,裏面有詳細的註釋。 oop
其主要功能就是 Query Engine 根據定義好的結構傳入起始點和要拓展的邊類型信息,而後存儲層會找到起始點,而後把該點的屬性和以該點的出邊的邊屬性找出來組裝成一個表格返回給 Query Engine,其中返回的表格的格式參考 github.com/vesoft-inc/… 中 GetNeighborsResponse 的定義,而後在 Query Engine 中咱們就能夠經過這個表格提取到咱們想要的內容。 例如在 basketba l l 數據集中,當起始點爲 Tim Duncan、Manu Ginobili 沿着 like
邊雙向拓展。想要得到 $^.[player.name](http://player.name/)
、like._dst
、$$.[player.name](http://player.name/)
和 like.likeness
這四個屬性。其返回的數據大體以下所示:fetch
表格1flex
由於是雙向拓展第四列的 + like
表明出邊,第五列的 - like
表明入邊。
在 Nebula Graph 的存儲層中邊是和起始點在一塊兒存放的,因此經過 getNeighbor 接口就能夠得到起點和出邊的全部屬性信息,可是若是想要在拓展過程當中拿到目的點的屬性信息則須要使用 getProps 接口,固然若是我只想經過 fetch 語句拿到某個點或者邊的屬性也須要調用這個接口。你能夠自行了解 github.com/vesoft-inc/… 下 getPropRequest 的定義,加深理解。
有了上面的接口定義咱們就能夠開始執行計劃了,首先須要的算子有 start、getNeighbor、subgraph、loop、datacollect。
PIPE
中會使用當前暫且不表,condition 至關於終止條件。loopBody 至關於 while 中的循環體。_dst
(目的點)屬性提出來而後過濾掉已經訪問過的目的點(避免重複從存儲層拿數據),而後把它們看成 getNeighbor 算子下一次拓展時的輸入。其中各個算子的詳細信息,可參考源碼 github.com/vesoft-inc/… 。 下面經過圖1 舉例,咱們是如何構建子圖的
圖1
拓展一步的狀況
當從 A 點開始沿着 like
邊只獲取一步的全部點和邊的信息,則很容易。只須要 getNeighbor 和 dataCollect 這兩個算子就能夠了。執行計劃以下圖所示 :
拓展多步的狀況
一步場景實際上是多步的場景的特殊狀況。因此能夠將一步的場景合入到多步場景中。當從 A 點開始,沿着 like 邊拓展三步的話,根據現有的算子,能夠在 getNeighbor 拓展後把目的點提取出來,而後將這些目的點看成起點從新調用 getNeighbor 接口,這個循環兩次就能夠了(loop 算子的終止條件設置爲當前步數),所以執行計劃以下圖所示 :
輸入和輸出
通常狀況下,每一個算子的輸入就是所依賴算子的輸出,這時候根據執行計劃的依賴關係就能夠直觀地肯定每一個算子的輸入和輸出。可是在某些狀況下,好比:子圖,在多步場景中每一次 getNeighbor 算子的輸入都應該是上一次拓展邊的目的點,也就是 subgraph 算子的輸出,所以 subgraph 算子的輸出應該就是 getNeighbor 算子的輸入。這時就和上圖的執行計劃依賴不一致,這時就須要自行設置每一個算子的輸入和輸出。在 Query Engine 2.0 中咱們已經介紹了每一個算子的輸入和輸出是存放在哈希表中的,其中 value 是 vector 類型。以下表 ResultMap 所示:
這時 getNeighbor 算子會把每一次的結果放在 ResultMap["GN_1"] 中的 vector 中的末尾,而後 subgraph 算子從 ResultMap["GN_1"] 中的 vector 中的末尾取值,通過計算再把下一次要拓展的起始點存放在 ResultMap["StartVid"] 中。
當拓展第一步後,ResultMap 的結果以下:
爲了方便顯示,GetNeighbor 的結果只寫了 _dst
的屬性,實際上會帶上邊上全部的屬性和起始點的全部屬性,相似於表格 1。
subgraph 算子接收"GN_1"的輸入,提取 _dst
屬性,而後將結果放入"StartVid"中。當拓展第二步後,ResultMap 的結果以下:
當拓展第三步後,ResultMap 的結果以下:
最後 dataCollect 算子從 ResultMap["GN_1"] 中取出拓展過程當中遇到的全部點集和邊集,組裝成最終的結果返回給用戶。
下面執行一個子圖的實例看看在 Nebula Graph 中執行計劃的具體結構,打開 nebula-console, 切換 space 到 basketball, 輸入 EXPLAIN format="dot" GET SUBGRAPH 2 STEPS FROM 'Tim Duncan' IN like, serve
,這時候 nebula-console 會生成 dot 格式的數據,而後打開 Graphviz Online 這個網站,將生成的 dot 數據粘貼上去,就能夠看到以下結構:
其中 Start_0 算子是 loop 算子中 depend 的依賴,因爲沒有多語句或 PIPE 語句,所以不作任何處理。 以上爲本次子圖的講解,若是你在使用子圖或者其餘 Nebula 過程當中遇到問題,歡迎來論壇和咱們交流:discuss.nebula-graph.com.cn/
想要和其餘大廠交流圖數據庫技術嗎?NUC 2021 大會等你來交流:NUC 2021 報名傳送門