orientDB学习笔记(二)MATCH

简 介

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.尽量声明边的名称和方向

在已知边和边的方向的情况下,明确声明边和边的方向,这样可以减少图的遍历路径的数量。

MATCH实战

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

5.3.深度遍历查询

基于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 协议》,转载必须注明作者和本文链接
你还差得远呐!
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!