OrientDB圖遍歷SQL之TRAVERSE
- 2019 年 10 月 27 日
- 筆記
本文介紹的TRAVERSE語法是基於OrientDB3.0.x版本,所有的SQL在OrientDB3.0.4社區版本自帶的數據庫demodb下試驗,數據模型請參考demodb。
1.簡介
TRAVERSE主要用於對圖進行遍歷。基於深度搜索算法或者廣度搜索算法對圖進行有限制的盲目搜索。它返回一個符合遍歷條件的子圖。
2.TRAVERSE語法格式介紹
根據官方文檔,TRAVERSE的語法格式如下:
TRAVERSE <[class.]field>|*|any()|all() [FROM <target>] [ MAXDEPTH <number> | WHILE <condition> ] [LIMIT <max-records>] [STRATEGY <strategy>]
下面我們對主要的語法點作下簡要的介紹。
- 必須以TRAVERSE關鍵字開頭,大小寫不敏感。
- <[class.]field>|*|any()|all()
1.any() 和all() 在orientdb2.x支持該函數,在orientdb3.x試驗下來,已不支持該函數。
2.根據官方文檔TRAVERSE可以跟<field>字段。如下SQL,我們嘗試執行如下兩個SQL:
TRAVERSE Bio FROM (SELECT * FROM Profiles WHERE Id = 1) TRAVERSE Hello FROM (SELECT * FROM Profiles WHERE Id = 1) TRAVERSE out_HasFriend FROM (SELECT * FROM Profiles WHERE Id = 1)



分析:上圖中Bio是Profiles的普通字段,而Hello不是Profiles的字段,out_HasFriend是系統自動為Profiles創建的邊的record。TRAVERSE是基於relationship來遍歷的,普通字段不會觸發遍歷,而對於邊的遍歷也僅僅遍歷到邊這一層而已。上圖中展示一條記錄也是Id為1的根記錄,在TRAVERSE的查詢結果中查詢目標對象總會被查詢出來,而且深度為0。
3.TRAVERSE後可跟9個函數:out()|in()|both()|outV()|inV()|bothV()|outE()|inE()|bothE()
函數 |
示例 |
查詢目標 |
遍歷結果 |
方向 |
---|---|---|---|---|
out() |
TRAVERSE out() FROM V LIMIT 8 |
點 |
點 |
左指向右 |
|
TRAVERSE out('EdgeClass') FROM V LIMIT 8 |
點 |
點 |
左指向右 |
in() |
TRAVERSE in() FROM V LIMIT 8 |
點 |
點 |
右指向左 |
|
TRAVERSE in('EdgeClass') FROM V LIMIT 8 |
點 |
點 |
右指向左 |
both() |
TRAVERSE both() FROM V LIMIT 8 |
點 |
點 |
任意 |
|
TRAVERSE both('EdgeClass') FROM V LIMIT 8 |
點 |
點 |
任意 |
outE() |
TRAVERSE outE() FROM V LIMIT 8 |
點 |
邊 |
左指向右 |
|
TRAVERSE outE('EdgeClass') FROM V LIMIT 8 |
點 |
邊 |
左指向右 |
inE() |
TRAVERSE inE() FROM V LIMIT 8 |
點 |
邊 |
右指向左 |
|
TRAVERSE inE('EdgeClass') FROM V LIMIT 8 |
點 |
邊 |
右指向左 |
bothE() |
TRAVERSE bothE() FROM V LIMIT 8 |
點 |
邊 |
任意 |
|
TRAVERSE bothE('EdgeClass') FROM V LIMIT 8 |
點 |
邊 |
任意 |
outV() |
TRAVERSE outV() FROM E LIMIT 8 |
邊 |
點 |
左指向右 |
|
TRAVERSE outV('EdgeClass') FROM E LIMIT 8 |
邊 |
點 |
左指向右 |
inV() |
TRAVERSE inV() FROM E LIMIT 8 |
邊 |
點 |
右指向左 |
|
TRAVERSE outV('EdgeClass') FROM E LIMIT 8 |
邊 |
點 |
右指向左 |
bothV() |
TRAVERSE V() FROM E LIMIT 8 |
邊 |
點 |
任意 |
|
TRAVERSE V('EdgeClass') FROM E LIMIT 8 |
邊 |
點 |
任意 |
* |
TRAVERSE * FROM V LIMIT 8 |
點 |
點和邊 |
任意 |
|
TRAVERSE * FROM E LIMIT 8 |
邊 |
點和邊 |
任意 |
- <target> 遍歷的目標對象。可以是一個class、cluster、record id(s)、子查詢。
- <number> 定義最大的遍歷深度,0表示遍歷根結點,不允許設置為負數。
- <condition> 定義遍歷的結束條件。經常和變量$depth一起使用,後續會有例子說明。
- <max-records> 定義該命令可以返回結果的最大數量。
- <strategy> 定義遍歷的策略。可選值有
DEPTH_FIRST
(深度優先搜索)、BREATH_FIRST
(廣度優先搜索)。默認為DEPTH_FIRST
。後續會有例子說明。
注意:where關鍵字不能用於該SQL。
3.TRAVERSE的使用
3.1.在browse控制台中使用
TRAVERSE * FROM (SELECT * FROM Profiles WHERE Id = 1)

3.2.在graph控制台中使用
TRAVERSE * FROM (SELECT * FROM Profiles WHERE Id = 1)

3.3.使用API
Maven依賴如下:
<dependency> <groupId>com.orientechnologies</groupId> <artifactId>OrientDB-graphdb</artifactId> <version>3.0.4</version> </dependency> <dependency> <groupId>com.orientechnologies</groupId> <artifactId>OrientDB-core</artifactId> <version>3.0.4</version> </dependency> <dependency> <groupId>com.orientechnologies</groupId> <artifactId>OrientDB-client</artifactId> <version>3.0.4</version> </dependency>
測試代碼如下:
public class TraverseTest { public static void main(String[] args) { // 用戶名和密碼,請根據配置修改 OrientGraphFactory factory = new OrientGraphFactory("remote:localhost/demodb", "root", "root"); OrientGraphNoTx graphNoTx = factory.getNoTx(); // 執行TRAVERSE語句 Iterable<Element> iterable = graphNoTx.command( new OCommandSQL( "TRAVERSE * FROM (SELECT * FROM Profiles where Id = 1) LIMIT 10" )).execute(); Iterator<Element> it = iterable.iterator(); // 遍歷TRAVERSE返回的結果集 while (it.hasNext()) { Element ele = it.next(); System.out.println("@class=>" + ele.getProperty("@class") + ",rid=>" + ele.getId()); } graphNoTx.shutdown(); factory.close(); } }
4.SELECT、MATCH和TRAVERSE比較
4.1.使用優先級
在一般情況下按如下順序選擇使用:SELECT>MATCH>TRAVERSE。
一般SELECT性能最好,TRAVERSE最差。因為TRAVERSE是基於深度優先搜索或者廣度優先搜索的盲目搜索算法,它返回是一個子圖。
4.2.查詢環
對於有些場景下可能會涉及到環的模型,如下圖的環的模型。

根據此模型通過如下SQL在圖庫構造模型數據:
INSRET INTO V SET name = 'P0' INSRET INTO V SET name = 'P1' INSRET INTO V SET name = 'P2' CREATE EDGEE FROM (SELECT FROM V where name = 'P0') TO (SELECT FROM V where name = 'P1') CREATE EDGEE FROM (SELECT FROM V where name = 'P0') TO (SELECT FROM V where name = 'P2') CREATE EDGEE FROM (SELECT FROM V where name = 'P1') TO (SELECT FROM V where name = 'P0') CREATE EDGEE FROM (SELECT FROM V where name = 'P1') TO (SELECT FROM V where name = 'P2')
通過如下SQL,查看圖中的模型數據。
SELECT FROM V WHERE name = 'P0'

然後我們分別執行如下SQL,觀察查詢結果:
SELECT expand(out().out()) FROM V WHERE name = 'P0'

MATCH {class:v,WHERE:(name = 'P0')}-->{as:p2,maxDepth:2,depthAlias:da} RETURN da,p2.name ORDER BY da ASC

SELECT $depth,$path,* FROM (TRAVERSE OUT() FROM (SELECT FROM V WHERE name = 'P0'))

分析:根據上述執行結果:
- SELECT的返回結果為:P0和P2。
- MATCH的一度返回結果結果為:P1和P2,二度返回結果為:P0和P2
- TRAVERSE的一度返回結果為P1和P2,二度返回結果為空。
第一個out()的返回結果即一度返回結果是P1和P2,這個是沒有問題的。但對於第二個out(),SELECT和MATCH的二度返回結果P0是查詢到環了,而P1是因為一度和二度是同一個點。而TRAVERSE卻不存在這種問題。所以在有些場景下我們可以基於這三者的特性來綜合使用。
4.3.使用場景
SELECT一般適用於類似RDBMS的查詢需求,同時也可以使用此來查詢特定路徑的查詢需求。
SELECT COUNT(*) FROM V SELECT out().out() FROM Profiles WHERE Id = 1
MATCH一般適用基本確定的多條路徑的查詢。
MATCH {as:customer,class:Customers,where:(Phone = '+1400844724')}.out('HasVisited'), {as:customer}.out('HasStayed'), {as:customer}.out('HasEaten') RETURN DISTINCT customer
TRAVERSE一般適用於不確定路徑的查詢遍歷。
TRAVERSE * FROM V MAXDEPTH 4
但有些場景下可能需要要組合使用。所以具體還要基於使用場景使用,有些場景就是使用MATCH合適,有些場景下可能就是使用TRAVERSE合適。
5.TRAVERSE實戰
5.1.編寫注意事項
為了儘可能減少在遍歷過程的查詢範圍,提高遍歷性能,在寫TRAVERSE語句時注意如下事項:
- 盡量縮小查詢目標的範圍。
- 盡量指定邊的方向和名稱。
- 盡量設置查詢深度MAXDEPTH的大小。
- 盡量設置LIMIT的大小。
5.2.查詢目標
FROM後的對象,我們暫時稱之為查詢目標。根據語法介紹部分,查詢目標可以是一個class、cluster、record id(s)、子查詢。
TRAVERSE * FROM Profiles TRAVERSE * FROM cluster:profiles TRAVERSE * FROM #41:0 TRAVERSE * FROM [#41:0,#41:1] TRAVERSE * FROM (SELECT FROM Profiles WHERE Id = 1)
5.3.上下文變量的使用
TRAVERSE支持如下變量$current、$path和$depth,這幾個變量都要和SELECT一起使用。
- $current 遍歷的當前結點。也就是遍歷路徑上的最後一個node。
- $path 遍歷的路徑node集合。包括每條遍歷路徑上所有點或邊或者點邊的集合,這是一個很有用的變量,通過它可知道兩個點之間的所有路徑及路徑上經過的點和邊。
- $depth 遍歷路徑的深度。$depth也可與WHILE一起使用。
SELECT $path,$current,$depth,@class FROM (TRAVERSE * FROM V)

注意:TRAVERSE *時,遍歷的結果包括點和邊,遍歷的深度是包括邊的。
5.4.MAXDPTH的使用
MAXDEPTH用於設置TRAVERSE的遍歷深度。"MAXDEPTH N"和"WHILE $depth <=N",具有同樣的查詢結果。但區別是WHILE會評估到N+1度,然後捨棄N+1度的數據,所以平時在使用時建議使用MAXDEPTH。
TRAVERSE * FROM (SELECT FROM Profiles WHERE Id = 1) MAXDEPTH 2 LIMIT 10

TRAVERSE * FROM (SELECT FROM Profiles WHERE Id = 1) WHILE $depth <= 2 LIMIT 10

5.5.遍歷策略的使用
TRAVERSE支持兩種遍歷策略:
- DEPTH_FIRST,基於深度優先搜索算法。沿着樹的深度遍歷樹的節點,儘可能深的搜索樹的分支。當節點v的所有邊都己被探尋過,搜索將回溯到發現節點v的那條邊的起始節點。這一過程一直進行到已發現從源節點可達的所有節點為止。如果還存在未被發現的節點,則選擇其中一個作為源節點並重複以上過程,整個進程反覆進行直到所有節點都被訪問為止。如下圖基於深度搜索算法的遍歷順序。

- BREADTH_FIRST,基於深度優先搜索算法。沿着樹的寬度遍歷樹的節點,如果發現目標,則演算終止。如下圖基於廣度搜索算法的遍歷順序。

我們來驗證下圖兩種遍歷策略。先執行如下SQL,構造模型數據(基於深度優先搜索算法的選擇分支和插入順序有關,所以如下SQL在創建邊時對順序有所關注,方便後續驗證問題):
INSERT INTO V(name) VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12) CREATE EDGE FROM (SELECT FROM V WHERE name = 1) TO (SELECT FROM V WHERE name = 8) CREATE EDGE FROM (SELECT FROM V WHERE name = 1) TO (SELECT FROM V WHERE name = 7) CREATE EDGE FROM (SELECT FROM V WHERE name = 1) TO (SELECT FROM V WHERE name = 2) CREATE EDGE FROM (SELECT FROM V WHERE name = 8) TO (SELECT FROM V WHERE name = 12) CREATE EDGE FROM (SELECT FROM V WHERE name = 8) TO (SELECT FROM V WHERE name = 9) CREATE EDGE FROM (SELECT FROM V WHERE name = 2) TO (SELECT FROM V WHERE name = 6) CREATE EDGE FROM (SELECT FROM V WHERE name = 2) TO (SELECT FROM V WHERE name = 3) CREATE EDGE FROM (SELECT FROM V WHERE name = 9) TO (SELECT FROM V WHERE name = 11) CREATE EDGE FROM (SELECT FROM V WHERE name = 9) TO (SELECT FROM V WHERE name = 10) CREATE EDGE FROM (SELECT FROM V WHERE name = 3) TO (SELECT FROM V WHERE name = 5) CREATE EDGE FROM (SELECT FROM V WHERE name = 3) TO (SELECT FROM V WHERE name = 4)
在OrientDB中的圖展示如下:
SELECT FROM V WHERE name = 1

可通過如下SQL來驗證,通過調整LIMIT的大小來觀察兩種遍歷策略的情況。
TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 1 STRATEGY DEPTH_FIRST TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 2 STRATEGY DEPTH_FIRST . . . TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 12 STRATEGY DEPTH_FIRST
TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 1 STRATEGY BREADTH_FIRST TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 2 STRATEGY BREADTH_FIRST . . . TRAVERSE out() FROM (SELECT FROM V WHERE name = 1) LIMIT 12 STRATEGY BREADTH_FIRST
限於篇幅,請自行驗證。
5.6.基於使用場景的選擇
場景:查詢Id=1的Profiles的二度朋友。
根據4.1.使用優先級我們分別按SELECT、MATCH和TRAVERSE的實現如下:
SELECT:
SELECT out('HasFriend').out('HasFriend') FROM Profiles WHERE Id = 1

MATCH:
MATCH {class:Profiles,WHERE:(Id = 1)}-HasFriend->{}-HasFriend->{as:friend} RETURN friend LIMIT 100

TRAVERSE:
SELECT * FROM (TRAVERSE out('HasFriend') FROM (SELECT * FROM Profiles WHERE Id = 1) MAXDEPTH 2) WHERE $depth = 2

分析:根據上述結果SELECT的返回結果數量為45,MATCH的返回結果數量也是45,且通過對比SELECT和MATCH的返回結果是一致的。但是TRAVERSE的返回結果卻是空。那麼究竟哪個是正確的呢?
我們先看下這Id=1的二度及以內的朋友關係子圖。
MATCH {class:Profiles,WHERE:(Id = 1)}-HasFriend->{}-HasFriend->{as:friend} RETURN $pathElements LIMIT 100

通過這個子圖,我們知道所有二度的朋友同時也是一度的朋友,是4.2.查詢環所描述的情形的一種。此種場景下如果有明確的需求要求二度朋友不能是一度朋友,那麼只有TRAVERSE的結果是滿足需求的。
但如果需求要求二度朋友也可以是一度朋友,那麼就要使用SELECT或者MATCH了。但根據上圖,即使二度朋友也可以是一度朋友,那麼二度朋友的數量應該是9(Id=2的Profiles不是二度朋友),而不是45才對。問題出在哪兒了?因為多個一度朋友可能有同一個二度朋友,所以二度朋友存在重複了,可藉助set()函數或者distinct關鍵字去重,去重SQL如下。
SELECT set(out('HasFriend').out('HasFriend')) FROM Profiles WHERE Id = 1

MATCH {class:Profiles,WHERE:(Id = 1)}-HasFriend->{}-HasFriend->{as:friend} RETURN distinct friend LIMIT 100

MATCH {class:Profiles,WHERE:(Id = 1)}-HasFriend->{}-HasFriend->{as:friend} RETURN set(friend) LIMIT 100

至此SELECT和MATCH好像已經完美的解決問題了,對於此模型是沒有問題,因為它不存在4.2.查詢環提到的P0-P1且P1->P0的情況,即一度朋友可能是Id=1的Profiles本身,二度朋友也可能是一度朋友本身或者是Id=1的Profiles本身。假如存在這種問題,怎麼辦?這種場景下SELECT已經很難實現,我們直接放棄,我們需要修改MATCH。如下SQL:
MATCH {as:p0,class:Profiles,WHERE:(Id = 1)}-HasFriend->{as:friend1,WHERE:($currentMatch != $matched.p0)}-HasFriend->{as:friend2,WHERE:($currentMatch != $matched.p0 AND $currentMatch != $matched.friend1)} RETURN set(friend2) LIMIT 100

總結:最終應該使用哪種SQL,還是要根據具體場景及需求來選擇。