orientDB學習筆記(二)MATCH

程式設計師的貓發表於2021-12-24

簡 介

MATCH是orientdb 2.2版本引入的以宣告方式的模式匹配語言,主要用於查詢圖。是OrientDB最靈活最有效的查詢圖的SQL。它和Neo4j的cypher語言有點像,但目前MATCH僅支援用於查詢。

MATCH語法格式介紹

根據官方文件,MATCH的語法格式如下:

MATCH 
  {
    [class: <class>], 
    [as: <alias>], 
    [where: (<whereCondition>)]
  }
  .<functionName>(){
    [class: <className>], 
    [as: <alias>], 
    [where: (<whereCondition>)], 
    [while: (<whileCondition>)],
    [maxDepth: <number>],    
    [depthAlias: <identifier> ], 
    [pathAlias: <identifier> ],     
    [optional: (true | false)]
  }*
  [,
    [NOT]
    {
      [as: <alias>], 
      [class: <class>], 
      [where: (<whereCondition>)]
    }
    .<functionName>(){
      [class: <className>], 
      [as: <alias>], 
      [where: (<whereCondition>)], 
      [while: (<whileCondition>)],
      [maxDepth: <number>],    
      [depthAlias: <identifier> ], 
      [pathAlias: <identifier> ],     
      [optional: (true | false)]
    }*
  ]*
RETURN [DISTINCT] <expression> [ AS <alias> ] [, <expression> [ AS <alias> ]]*
GROUP BY <expression> [, <expression>]*
ORDER BY <expression> [, <expression>]*
SKIP <number>
LIMIT <number>

下面我們對主要的語法點作下簡要的介紹。

  • 必須以MATCH關鍵字開頭,大小寫不敏感。

  • {}用於對一個node進行定義及條件過濾,這個node可以是點也可以是邊。

  • []表示可選項。{}內所有的定義都是可選的,也就是說可以直接寫成{}。

  • <>表示具體的值。

  • 定義一個有效的class,可以是一個點也可以是一個邊。

  • 為node定義一個別名,在整個模式中可以根據這個別名來訪問這個node,類似於SQL中table的別名。

  • 定義匹配當前node的過濾條件,它支援大部分SQL中的where語法。同時也可以使用兩個上下文變數$currentMatch和$matched,具體如何使用這兩個變數,後續會有例子詳細解釋。

  • 定義一個用於表示連線兩個node的圖函式。它支援的函式有:out()、in()、both()、outE()、inE()、bothE()、outV()、inV()、bothV()。對於out()、in()和both()也可以用更形象化的箭頭表示法。下面我們著重對這9個函式作下詳細的介紹,注意右邊的node不是必須存在的。

    函式 示例 箭頭表示法 左邊 右邊 方向
    out() {…}.out(){…} {…}–>{…} 左指向右
    {…}.out(“EdgeClass”){…} {…}-EdgeClass->{…} 左指向右
    in() {…}.in(){…} {…}<–{…} 右指向左
    {…}.in(“EdgeClass”){…} {…}<-EdgeClass-{…} 右指向左
    both() {…}.both(){…} {…}–{…} 任意
    {…}.both(“EdgeClass”){…} {…}-EdgeClass-{…} 任意
    outE() {…}.outE(){…} 左指向右
    {…}.outE(“EdgeClass”){…} 左指向右
    inE() {…}.inE(){…} 右指向左
    {…}.inE(“EdgeClass”){…} 右指向左
    bothE() {…}.bothE() {…} 任意
    {…}.bothE(“EdgeClass”){…} 任意
    outV() {…}.outV() {…} 左指向右
    {…}.outV(“EdgeClass”){…} 左指向右
    inV() {…}.inV() {…} 右指向左
    {…}.inV(“EdgeClass”){…} 右指向左
    bothV() {…}.bothV() {…} 任意
    {…}.bothV(“EdgeClass”){…} 任意
  • 定義深度遍歷路徑上滿足的條件,它支援大部分SQL中的where語法,同時也可以使用上下文變數$currentMatch、$matched、$depth,具體如何使用這些變數,後續會有例子詳細解釋。

  • 定義深度遍歷的最大深度,後續會有例子詳細解釋。

  • orientdb3.X新增加的特性,必須和while或者maxDepth一起使用,該值用於儲存遍歷的深度,在return中可以通過該值獲取每次遍歷深度的值。

  • orientdb3.X新增加的特性,必須和while或者maxDepth一起使用,該值用於儲存遍歷的路徑,在return中可以通過該值獲取每次遍歷路徑下的點。

  • optional 是orientdb2.2.4版本新增的特性。在預設情況下該選項的值為false,它的意思是所宣告的結點必須存在,否則不會匹配該條路徑上的資料。如果設定為true,那麼即使這個節點沒有匹配到,也不會影響整條路徑的匹配,但這個選項只能出現在路徑上最右邊的節點。類似於SQL中的left join。

  • RETURN [ AS ] 定義返回的資料結構。返回值包括如下三種:{…}中定義的別名、別名.欄位以及上下文變數。RETURN可使用的上下文變數詳細解釋:

    變數名稱 解釋 備註
    $matches 包括所有在{…}定義了別名的node。
    $paths 包括所有遍歷路徑上node。包括沒有定義別名的node。
    $elements 包括$matches返回的node展開的資料。 可以在graph控制檯上以圖的形式展示
    $pathElements 包括$paths返回的node展開的資料。 可以在graph控制檯上以圖的形式展示
  • DISTINCT 3.X版本支援對RETURN的結果進行去重。注意3.X之前是不支援這個特性,需要通過在外層套一層SELECT然後DISTINCT去重。

  • GROUP BY 分組。3.X引入的特性。

  • ORDER BY 排序。3.X引入的特性。

  • SKIP 和LIMIT一起可進行分頁。3.X引入的特性。

MATCH的使用

3.1.在browse控制檯中使用

  1. MATCH

  2. {as:c,class:Customers,where:(Phone='+1400844724')}

  3. RETURN c.Phone,c.OrderId

3.2.在graph控制檯中使用

在graph中以圖的形式顯示資料,需要藉助$pathElements或者$elements變數。

  1. MATCH

  2. {as:c,class:Customers,where:(Phone='+1400844724')}

  3. RETURN $pathElements

3.3.使用API

maven依賴如下:

  1. <dependency>

  2. <groupId>com.orientechnologies</groupId>

  3. <artifactId>OrientDB-graphdb</artifactId>

  4. <version>3.0.4</version>

  5. </dependency>

  6. <dependency>

  7. <groupId>com.orientechnologies</groupId>

  8. <artifactId>OrientDB-core</artifactId>

  9. <version>3.0.4</version>

  10. </dependency>

  11. <dependency>

  12. <groupId>com.orientechnologies</groupId>

  13. <artifactId>OrientDB-client</artifactId>

  14. <version>3.0.4</version>

  15. </dependency>

測試程式碼如下:

  1. public class MatchTest {

  2. public static void main(String[] args) {

  3. // 使用者名稱和密碼,請根據配置修改

  4. OrientGraphFactory factory = new OrientGraphFactory("remote:localhost/demodb", "root", "root");

  5. OrientGraphNoTx graphNoTx = factory.getNoTx();

  6. // 執行MATCH語句

  7. Iterable<Element> iterable = graphNoTx.command(

  8. new OCommandSQL(

  9. "MATCH {as:c,class:Customers,where:(Phone='+1400844724')} RETURN c.Phone as phone,c.OrderedId as orderedId"

  10. )).execute();

  11. // 遍歷MATCH返回的結果集

  12. Iterator<Element> it = iterable.iterator();

  13. while (it.hasNext()) {

  14. Element ele = it.next();

  15. System.out.println("Phone=>" + ele.getProperty("phone") + ",OrderedId=>" + ele.getProperty("orderedId"));

  16. }

  17. graphNoTx.shutdown();

  18. factory.close();

  19. }

  20. }

編寫MATCH語句的規則

4.1.確定查詢的起始點

圖查詢要從一個或者多個node開始,否則會引起某個class的全表掃描,甚至會引導整個圖的遍歷,這個開始的node就是查詢的起始點。全表掃描時的效能可能不如RDBMS的效能。

起始點要根據查詢需求來判斷及確定。一般可根據已知的查詢條件能夠最快確定的點就認為是起始點。如根據使用者的手機號”+1400844724”查詢獲取使用者的朋友,那麼根據使用者的手機號找到使用者的記錄,然後根據已經找到使用者的點再去遍歷獲取使用者的朋友,而不能根據朋友找使用者。

確定了查詢的起始點後,我們就可以編寫match語句了,但要讓查詢引擎按照我們的想法執行,需要注意一些編寫注意事項。

4.2.MATCH中必須要有一個class顯示的宣告的node

如下SQL語句沒有顯示的宣告class,執行後報” java.lang.UnsupportedOperationException”。

  1. MATCH

  2. {as:customer,where:(Phone='+1400844724')}-HasProfile->{as:profile}-HasFriend-{as:friend}

  3. RETURN distinct customer,profile,friend

4.3.如果只有一個node宣告瞭class,那麼這個node就是起始點,無論這個node有沒有過濾條件

  1. MATCH

  2. {as:customer,where:(Phone='+1400844724')}-HasProfile->{as:profile}-HasFriend-{as:friend,class:Profiles}

  3. RETURN distinct customer,profile,friend

explain結果如下:

分析:根據explain的結果可以知道起始點是Profiles,雖然Profiles沒有設定過濾條件,但只有它指定了class。

4.4.起始點必須要顯示宣告class

只有宣告瞭class才有可能作為起始點。

4.5.起始點的過濾條件儘量加索引

  1. MATCH

  2. {as:customer,class:Customers,where:(Phone='+1400844724')}-HasProfile->{as:profile,class:Profiles,where:(Id=2)}-HasFriend-{as:friend,class:Profiles,where:(Id=1)}

  3. RETURN distinct customer,profile,friend

explain結果如下:

分析:根據上圖explain的結果可知道起始點是Profiles,且命中了索引Id=1和Id=9。Customers雖然宣告瞭class且新增了過濾條件,但並沒有把Customers作為起始點,因為Customers的Phone屬性上沒有索引。

請再explain如下SQL,觀察下explain的結果。

  1. MATCH

  2. {as:customer,class:Customers,where:(Phone='+1400844724')}-HasProfile->{as:profile,where:(Id=9)}-HasFriend-{as:friend,where:(Id=1)}

  3. RETURN distinct customer,profile,friend

4.6.非起始點儘量不要宣告class,避免執行引擎把它識別成起始結點

4.7.儘量宣告邊的名稱和方向

在已知邊和邊的方向的情況下,明確宣告邊和邊的方向,這樣可以減少圖的遍歷路徑的數量。

5.1.MATCH返回的結果的去重處理

建立兩個點,並且在兩個點之間建立三條邊,建立語句如下:

  1. insert into V set name = 'v1'

  2. insert into V set name = 'v2'

  3. create edge E from (select from V where name= 'v1') to (select from V where name= 'v2')

  4. create edge E from (select from V where name= 'v1') to (select from V where name= 'v2')

  5. create edge E from (select from V where name= 'v1') to (select from V where name= 'v2')

執行如下SQL語句:

  1. MATCH

  2. {as:v1,class:V,where:(name = 'v1')}--{as:v2}

  3. RETURN v1,v2

在orientdb3.0.x的執行結果如下。

分析:返回了3條記錄,說明沒有去重。但是orientdb3.x支援distinct關鍵字,可通過如下語句去重。

  1. MATCH

  2. {as:v1,class:V,where:(name = 'v1')}--{as:v2}

  3. RETURN distinct v1,v2

orientdb3.x這種設計更加合理,由使用者來自主選擇結果是否需要去重,而在orientdb2.x版本只返回了1條記錄,說明是自動去重的。在使用時需要注意,請自行驗證。

5.2.MATCH返回的結果的數量

MATCH返回的結果的數量是所有查詢路徑的數量。也可理解為根據所有起始結點查詢的笛卡兒積之和。

  1. MATCH

  2. {as:customer,class:Customers,where:(Phone in ['+1400844724','+1548604972'])}-HasProfile->{as:profile}-HasFriend->{as:friend1}

  3. RETURN customer,profile,friend1

執行結果如下:

分析:起始結點為Customers,根據查詢條件最多可查詢到兩個Customers記錄。

根據’+1400844724’查詢路徑的數量為:1(customer的數量) * 1(HasProfile的數量) * 1 * 1(profile的數量) * 1(邊HasFriend的數量) * 2(friend1的數量) = 2

根據’+1548604972’查詢路徑的數量為:1(customer的數量) * 1(HasProfile的數量) * 1 * 1(profile的數量) * 1(邊HasFriend的數量) * 2(friend1的數量) = 2

所以所有路徑的數量4 = 2 + 2

基於MATCH的深度遍歷查詢有兩個辦法:第一個使用maxDepth,第二個是while和$depth變數。示例獲取Id為9的Profiles的深度為2的朋友關係。

5.3.1.使用maxDepth進行深度遍歷

  1. MATCH

  2. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,depthAlias:da}

  3. RETURN friend.Id,da

執行結果如下:

分析:我們藉助orientdb3.x版本提供的depthAlilas特性獲取到friend的深度,方便我們理解。根據上圖的執行結果當maxDepth為2時,獲取的資料包括深度為0(查詢起始點)、1、2的資料。

深度為0是查詢起始結點,如何剔除深度為0的資料呢?有兩個辦法:

1)、使用MATCH和SELECT的組合

  1. select

  2. *

  3. from (

  4. MATCH

  5. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,depthAlias:da}

  6. RETURNfriend.Id,da

  7. )

  8. where da > 0

2)、使用$depth進行過濾

  1. MATCH

  2. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,where:($depth > 0),depthAlias:da}

  3. RETURN friend.Id,da

限於篇幅請自行驗證結果。

5.3.2.使用while和$depth進行深度遍歷

  1. MATCH

  2. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),depthAlias:da}

  3. RETURN friend.Id,da

分析:根據上圖的執行結果當$depth<2時,獲取的資料包括深度為0(查詢起始點)、1、2的資料,注意這裡包括深度為2的資料。

深度為0是查詢起始結點,如何剔除深度為0的資料呢?

有兩個辦法:

1)、使用MATCH和SELECT的組合

  1. select

  2. *

  3. from (

  4. MATCH

  5. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),depthAlias:da}

  6. RETURNfriend.Id,da

  7. )

  8. where da > 0

2)、使用$depth進行過濾

  1. MATCH

  2. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:(depth < 2),where:(depth > 0),depthAlias:da}

  3. RETURN friend.Id,da

限於篇幅大家自行驗證結果。

5.4.RETURN上下文變數的使用

請執行如下SQL結合MATCH語法描述部分理解下這幾個變數不同。限於篇幅,請自行驗證結果。

  1. MATCH

  2. {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){}

  3. RETURN $matches

  4. MATCH

  5. {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){}

  6. RETURN $paths

  7. MATCH

  8. {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){}

  9. RETURN $elements

  10. MATCH

  11. {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){}

  12. RETURN $pathElements

5.5.使用count(*)而不是count(1)

在關係型資料庫中我們建議使用count(1)統計數量,但在orientdb中我們建議使用count(*)而不是count(1)。具體原因,我們通過explain如下SQL來分析下。

  1. select count(*) from Profiles

explain結果如下:

  1. select count(1) from Profiles

explain結果如下:

分析:根據explain的結果:count()是直接取class的記錄大小,而count(1)會取出所有class下cluster的記錄,然後計算大小,所以count()的效能肯定比count(1)快很多。但由於本例中Profiles的數量比較小,效能上看不出大的差別,倘若數量大,效能會有明顯的差別。可自行驗證資料量比較大的點。

5.6.分組查詢

統計Id為9的朋友一度和二度朋友的數量。

  1. MATCH

  2. {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),where:($depth > 0),depthAlias:da}

  3. RETURN da,count(*)

  4. GROUP BY da

  5. ORDER BY da desc

使用了orientdb3.x提供的group by和order by功能。

5.7.分頁查詢

分頁查詢friend,獲取第9頁,每頁10條記錄。

  1. MATCH

  2. {as:profile,class:Profiles}

  3. RETURN profile

  4. SKIP 90

  5. LIMIT 100

explain結果如下:

分析:分頁查詢需要skip和limit一起使用,其思路和mysql的limit分頁是一致的。需要查詢出前limit條,然後通過skip跳過來分頁,當資料量大且查詢頁數越大時查詢效能越慢。使用時請慎重使用,可考慮基於索引限制條件來分頁。

5.8.拆分SQL語句

假如有些場景的查詢需要一個點與三條以及上的邊關聯,那麼如何寫這個SQL呢?按照我們目前理解的MATCH寫法一個node只能左邊關聯一個node,右邊關聯一個node。這個地方就需要拆分 SQL語句。

找一個客戶,這個客戶既有吃過某個酒店,也居住過某個酒店,也訪問過某些旅遊景點。SQL如下:

  1. MATCH

  2. {as:customer,class:Customers,where:(Phone = '+1400844724')}.out('HasVisited'), {as:customer}.out('HasStayed'),

  3. {as:customer}.out('HasEaten')

  4. RETURN distinct customer

5.9.實現LEFT JOIN的功能

查詢出所有Customers,要求Customers必須要有Friend,SQL如下:

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend}

  3. RETURN customer,friend

那麼如果查詢出所有Customers同時帶出它們的Friend,即使沒有Friend的Customers也要查詢出來。那麼該如何寫呢?

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend,optional:true} RETURN customer,friend

5.10.實現INNER JOIN的功能

查詢出所有Customers,要求它的Name和它的朋友的Name相同。

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{as:friend,where:($matched.profile.Name = Name)}

  3. RETURN distinct customer,profile,friend

分析:藉助$matched變數引用另外一個點的別名,然後通過別名訪問相關屬性。這個示例中的資料顯示它自己是自己的朋友,這個僅說明如何使用,不用太關心具體的業務資料。

5.11.已知RID查詢

已知Customers的rid為#121:0,查詢出它的朋友。

  1. MATCH

  2. {as:customer,rid:#121:0}-HasProfile->{as:profile}-HasFriend-{as:friend}

  3. RETURN distinct customer,profile,friend

分析:這個特性orientdb官方並沒有暴露出來,雖然目前試驗下來orientdb2.x和orientdb3.x都支援,但請慎重使用,說不定下個版本應當不支援了。

5.12.基於邊上的條件查詢

查詢在2018-10-17這個日期成為朋友的使用者和朋友。

由於邊HasFriend上的屬性From和SQL關鍵字衝突,無法根據此屬性查詢,所以我們需要新建個屬性,執行SQL:

  1. update edge HasFriend set since = '2018-10-17' limit 1

然後執行如下SQL:

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{as:profile}.bothE('HasFriend'){where:(since = '2018-10-17')}.bothV(){as:friend}

  3. RETURN distinct customer,profile,friend

5.13.如何避免查詢環

查詢所有Customers的朋友的朋友。

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{}-HasFriend-{as:friend2}

  3. RETURN distinct customer,profile,friend2 limit 10

通過上圖查詢結果高亮部分我們可以知道用的朋友的朋友是它自己,形成了一個查詢環,這樣的資料應該剔除,那麼如何剔除呢?需要藉助兩個變數$matched和$currentMatch,$matched前邊例子已經介紹過,要想在{}點內訪問另外一個{}點,必須藉助$matched。不過訪問當前{}點也是不能直接使用當前{}點定義的別名的,需要藉助$currentMatch。

  1. MATCH

  2. {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{}-HasFriend-{as:friend2,where:($currentMatch != $matched.profile)}

  3. RETURN distinct customer,profile,friend2 limit 10

本作品採用《CC 協議》,轉載必須註明作者和本文連結
你還差得遠吶!

相關文章