本文首發於 Nebula Graph 公眾號 NebulaGraphCommunity,Follow 看大廠圖數據庫技術實踐。
前言
在先前的 Query Engine 源碼解析中,我們介紹了 2.0 中 Query Engine 和 1.0 的主要變化和大體的結構:
大家可以大概了解到用戶通過客戶端發送一條查詢語句,Query Engine 是如何解析語句、把語句構建為抽象語法樹,在抽象語法樹進行校驗、生成執行計划的過程。本文會通過 2.0 中新增的子圖算法模塊繼續講解 Query Engine 背后所做的內容,並着重介紹執行計划生成的過程,以便加強你對源碼更好地理解。
子圖的定義
子圖是指節點集合和邊集合分別是某一圖的節點集的子集和邊集的子集的圖。直觀地理解,就是從用戶指定的起點開始出發沿着指定的邊一步步拓展,直到達到用戶所設定的步數為止,然后返回在拓展過程中遇到的所有點集和邊集。
子圖的語法
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:指定從起始點開始的跳數,返回從 0 到
step_count
跳的子圖。必須是非負整數。默認值為 1 - vid:指定起始點 ID
- edge_type:指定邊類型。可以用
IN
、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 需要了解一下。
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
}
該結構中每個變量的詳細定義可以參考 https://github.com/vesoft-inc/nebula-common/blob/master/src/common/interface/storage.thrift,里面有詳細的注釋。
其主要功能就是 Query Engine 根據定義好的結構傳入起始點和要拓展的邊類型信息,然后存儲層會找到起始點,然后把該點的屬性和以該點的出邊的邊屬性找出來組裝成一個表格返回給 Query Engine,其中返回的表格的格式參考 https://github.com/vesoft-inc/nebula-common/blob/master/src/common/interface/storage.thrift 中 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
這四個屬性。其返回的數據大致如下所示:
表格1
因為是雙向拓展第四列的 + like
代表出邊,第五列的 - like
代表入邊。
在 Nebula Graph 的存儲層中邊是和起始點在一起存放的,所以通過 getNeighbor 接口就可以獲得起點和出邊的所有屬性信息,但是如果想要在拓展過程中拿到目的點的屬性信息則需要使用 getProps 接口,當然如果我只想通過 fetch 語句拿到某個點或者邊的屬性也需要調用這個接口。你可以自行了解 https://github.com/vesoft-inc/nebula-common/blob/master/src/common/interface/storage.thrift 下 getPropRequest 的定義,加深理解。
執行計划
有了上面的接口定義我們就可以開始執行計划了,首先需要的算子有 start、getNeighbor、subgraph、loop、datacollect。
- start 算子:相當於執行計划中葉子節點,不做任何事情。目的是告訴調度器,之后沒有可以依賴的算子,或者可以理解為遞歸算法中的終止條件。
- loop 算子:相當於 C 語言中的 while 語法,該算子有三個成員 depend、condition 和 loopBody,depend 在多語句和
PIPE
中會使用當前暫且不表,condition 相當於終止條件。loopBody 相當於 while 中的循環體。 - subgraph 算子:負責把 getNeighbor 算子結果中的
_dst
(目的點)屬性提出來然后過濾掉已經訪問過的目的點(避免重復從存儲層拿數據),然后把它們當作 getNeighbor 算子下一次拓展時的輸入。 - datacollect 算子:負責在最后把拓展過程中獲得的點和邊屬性收集起來組裝為 vertex 和 edge 類型。
其中各個算子的詳細信息,可參考源碼 https://github.com/vesoft-inc/nebula-graph/tree/master/src/executor 。
下面通過圖1 舉例,我們是如何構建子圖的
圖1
拓展一步的情況
當從 A 點開始沿着 like
邊只獲取一步的所有點和邊的信息,則很容易。只需要 getNeighbor 和 dataCollect 這兩個算子就可以了。執行計划如下圖所示 :
拓展多步的情況
一步場景其實是多步的場景的特殊情況。所以可以將一步的場景合入到多步場景中。當從 A 點開始,沿着 like 邊拓展三步的話,根據現有的算子,可以在 getNeighbor 拓展后把目的點提取出來,然后將這些目的點當作起點重新調用 getNeighbor 接口,這個循環兩次就可以了(loop 算子的終止條件設置為當前步數),因此執行計划如下圖所示 :
輸入和輸出
一般情況下,每個算子的輸入就是所依賴算子的輸出,這時候根據執行計划的依賴關系就可以直觀地確定每個算子的輸入和輸出。但是在某些情況下,比如:子圖,在多步場景中每一次 getNeighbor 算子的輸入都應該是上一次拓展邊的目的點,也就是 subgraph 算子的輸出,因此 subgraph 算子的輸出應該就是 getNeighbor 算子的輸入。這時就和上圖的執行計划依賴不一致,這時就需要自行設置每個算子的輸入和輸出。在 Query Engine 2.0 中我們已經介紹了每個算子的輸入和輸出是存放在哈希表中的,其中 value 是 vector 類型。如下表 ResultMap 所示:
- 起始點存放在 ResultMap["StartVid"] 中
- getNeighbor 算子的輸入是 ResultMap["StartVid"], 輸出存放在 ResultMap["GN_1"]
- subgraph 算子的輸入是 ResultMap["GN_1"], 輸出存放在 ResultMap["StartVid"]
- loop 算子不產生數據,當作邏輯循環使用,因此不需要設置輸入輸出
- dataCollect 算子的輸入是 ResultMap["GN_1"], 輸出存放在 ResultMap["DATACOLLECT_2"]
這時 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 過程中遇到問題,歡迎來論壇和我們交流:https://discuss.nebula-graph.com.cn/
想要和其他大廠交流圖數據庫技術嗎?NUC 2021 大會等你來交流:NUC 2021 報名傳送門