前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >OrientDB图遍历SQL之MATCH

OrientDB图遍历SQL之MATCH

作者头像
曲水流觞
发布2019-10-27 22:01:16
4.1K7
发布2019-10-27 22:01:16
举报
文章被收录于专栏:曲水流觞TechRill曲水流觞TechRill

本文介绍的MATCH语法是基于orientdb3.0.x版本,所有的SQL在orientdb3.0.4社区版本自带的数据库demodb下试验,数据模型请参考demodb。本文力求对MATCH做个系统性的讲解,所以文章的第2章节专门对MATCH的语法作了详细的解释。

目 录

1.简介

2.MATCH语法格式介绍

3.MATCH的使用

3.1.在browse控制台中使用

3.2.在graph控制台中使用

3.3.使用API

4.编写MATCH语句的规则

4.1.确定查询的起始点

4.2.MATCH中必须要有一个class显示的声明的node

4.3.如果只有一个node声明了class,那么这个node就是起始点,无论这个node有没有过滤条件

4.4.起始点必须要显示声明class

4.5.起始点的过滤条件尽量加索引

4.6.起始点尽量不要声明class,避免执行引擎把它识别成起始结点

4.7.尽量声明边的名称和方向

5.MATCH实战

5.1.MATCH返回的结果的去重处理

5.2.MATCH返回的结果的数量

5.3.深度遍历查询

5.3.1.使用maxDepth进行深度遍历

5.3.2.使用while和$depth进行深度遍历

5.4.RETURN上下文变量的使用

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

5.6.分组查询

5.7.分页查询

5.8.拆分SQL语句

5.9.实现LEFT JOIN的功能

5.10.实现INNER JOIN的功能

5.11.已知RID查询

5.12.基于边上的条件查询

5.13.如何避免查询环

6.练习

01 简介

MATCH是orientdb 2.2版本引入的以声明方式的模式匹配语言,主要用于查询图。是OrientDB最灵活最有效的查询图的SQL。它和Neo4j的cypher语言有点像,但目前MATCH仅支持用于查询。

02 MATCH语法格式介绍

根据官方文档,MATCH的语法格式如下:

代码语言:javascript
复制
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> ][, [ AS ]]*
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引入的特性。

03 MATCH的使用

3.1.在browse控制台中使用

代码语言:javascript
复制
MATCH 
    {as:c,class:Customers,where:(Phone='+1400844724')}
RETURN c.Phone,c.OrderId

3.2.在graph控制台中使用

在graph中以图的形式显示数据,需要借助$pathElements或者$elements变量。

代码语言:javascript
复制
MATCH 
    {as:c,class:Customers,where:(Phone='+1400844724')}
RETURN $pathElements

3.3.使用API

maven依赖如下:

代码语言:javascript
复制
<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>

测试代码如下:

代码语言:javascript
复制
public class MatchTest {
    public static void main(String[] args) {
        // 用户名和密码,请根据配置修改
        OrientGraphFactory factory = new OrientGraphFactory("remote:localhost/demodb", "root", "root");
        OrientGraphNoTx graphNoTx = factory.getNoTx();
        // 执行MATCH语句
        Iterable<Element> iterable = graphNoTx.command(
                new OCommandSQL(
                        "MATCH {as:c,class:Customers,where:(Phone='+1400844724')} RETURN c.Phone as phone,c.OrderedId as orderedId"
                )).execute();
        // 遍历MATCH返回的结果集
        Iterator<Element> it = iterable.iterator();
        while (it.hasNext()) {
            Element ele = it.next();
            System.out.println("Phone=>" + ele.getProperty("phone") + ",OrderedId=>" + ele.getProperty("orderedId"));
        }
        graphNoTx.shutdown();
        factory.close();
    }
}

04 编写MATCH语句的规则

4.1.确定查询的起始点

图查询要从一个或者多个node开始,否则会引起某个class的全表扫描,甚至会引导整个图的遍历,这个开始的node就是查询的起始点。全表扫描时的性能可能不如RDBMS的性能。

起始点要根据查询需求来判断及确定。一般可根据已知的查询条件能够最快确定的点就认为是起始点。如根据用户的手机号”+1400844724”查询获取用户的朋友,那么根据用户的手机号找到用户的记录,然后根据已经找到用户的点再去遍历获取用户的朋友,而不能根据朋友找用户。

确定了查询的起始点后,我们就可以编写match语句了,但要让查询引擎按照我们的想法执行,需要注意一些编写注意事项。

4.2.MATCH中必须要有一个class显示的声明的node

如下SQL语句没有显示的声明class,执行后报” java.lang.UnsupportedOperationException”。

代码语言:javascript
复制
MATCH 
    {as:customer,where:(Phone='+1400844724')}-HasProfile->{as:profile}-HasFriend-{as:friend} 
RETURN distinct customer,profile,friend

4.3.如果只有一个node声明了class,那么这个node就是起始点,无论这个node有没有过滤条件

代码语言:javascript
复制
MATCH   
    {as:customer,where:(Phone='+1400844724')}-HasProfile->{as:profile}-HasFriend-{as:friend,class:Profiles}   
RETURN distinct customer,profile,friend

explain结果如下:

分析:根据explain的结果可以知道起始点是Profiles,虽然Profiles没有设置过滤条件,但只有它指定了class。

4.4.起始点必须要显示声明class

只有声明了class才有可能作为起始点。

4.5.起始点的过滤条件尽量加索引

代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone='+1400844724')}-HasProfile->{as:profile,class:Profiles,where:(Id=2)}-HasFriend-{as:friend,class:Profiles,where:(Id=1)}   
RETURN distinct customer,profile,friend

explain结果如下:

分析:根据上图explain的结果可知道起始点是Profiles,且命中了索引Id=1和Id=9。Customers虽然声明了class且添加了过滤条件,但并没有把Customers作为起始点,因为Customers的Phone属性上没有索引。

请再explain如下SQL,观察下explain的结果。

代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone='+1400844724')}-HasProfile->{as:profile,where:(Id=9)}-HasFriend-{as:friend,where:(Id=1)}   
RETURN distinct customer,profile,friend

4.6.非起始点尽量不要声明class,避免执行引擎把它识别成起始结点

4.7.尽量声明边的名称和方向

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

05 MATCH实战

5.1.MATCH返回的结果的去重处理

创建两个点,并且在两个点之间创建三条边,创建语句如下:

代码语言:javascript
复制
insert into V set name = 'v1'   
insert into V set name = 'v2'   
create edge E from (select from V where name= 'v1') to (select from V where name= 'v2') 
create edge E from (select from V where name= 'v1') to (select from V where name= 'v2') 
create edge E from (select from V where name= 'v1') to (select from V where name= 'v2')

执行如下SQL语句:

代码语言:javascript
复制
MATCH
    {as:v1,class:V,where:(name = 'v1')}--{as:v2} 
RETURN v1,v2

在orientdb3.0.x的执行结果如下。

分析:返回了3条记录,说明没有去重。但是orientdb3.x支持distinct关键字,可通过如下语句去重。

代码语言:javascript
复制
MATCH
    {as:v1,class:V,where:(name = 'v1')}--{as:v2} 
RETURN distinct v1,v2

orientdb3.x这种设计更加合理,由使用者来自主选择结果是否需要去重,而在orientdb2.x版本只返回了1条记录,说明是自动去重的。在使用时需要注意,请自行验证。

5.2.MATCH返回的结果的数量

MATCH返回的结果的数量是所有查询路径的数量。也可理解为根据所有起始结点查询的笛卡儿积之和。

代码语言:javascript
复制
MATCH
   {as:customer,class:Customers,where:(Phone in ['+1400844724','+1548604972'])}-HasProfile->{as:profile}-HasFriend->{as:friend1}      
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进行深度遍历

代码语言:javascript
复制
MATCH     
    {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,depthAlias:da} 
RETURN friend.Id,da

执行结果如下:

分析:我们借助orientdb3.x版本提供的depthAlilas特性获取到friend的深度,方便我们理解。根据上图的执行结果当maxDepth为2时,获取的数据包括深度为0(查询起始点)、1、2的数据。

深度为0是查询起始结点,如何剔除深度为0的数据呢?有两个办法:

1)、使用MATCH和SELECT的组合

代码语言:javascript
复制
select 
    * 
from (  
  MATCH     
    {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,depthAlias:da}   
  RETURN  friend.Id,da
)   
where da > 0

2)、使用$depth进行过滤

代码语言:javascript
复制
MATCH   
    {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,maxDepth:2,where:($depth > 0),depthAlias:da}   
RETURN  friend.Id,da

限于篇幅请自行验证结果。

5.3.2.使用while和$depth进行深度遍历

代码语言:javascript
复制
MATCH   
    {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),depthAlias:da}   
RETURN  friend.Id,da

分析:根据上图的执行结果当$depth<2时,获取的数据包括深度为0(查询起始点)、1、2的数据,注意这里包括深度为2的数据。

深度为0是查询起始结点,如何剔除深度为0的数据呢?

有两个办法:

1)、使用MATCH和SELECT的组合

代码语言:javascript
复制
select 
     * 
from (   
    MATCH     
        {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),depthAlias:da}   
    RETURN  friend.Id,da
)   
where da > 0

2)、使用$depth进行过滤

代码语言:javascript
复制
MATCH 
   {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:(depth < 2),where:(depth >  0),depthAlias:da}   
RETURN friend.Id,da

限于篇幅大家自行验证结果。

5.4.RETURN上下文变量的使用

请执行如下SQL结合MATCH语法描述部分理解下这几个变量不同。限于篇幅,请自行验证结果。

代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} 
RETURN  $matches
代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} 
RETURN  $paths
代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} 
RETURN  $elements
代码语言:javascript
复制
MATCH
    {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} 
RETURN  $pathElements

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

在关系型数据库中我们建议使用count(1)统计数量,但在orientdb中我们建议使用count(*)而不是count(1)。具体原因,我们通过explain如下SQL来分析下。

代码语言:javascript
复制
select count(*) from Profiles

explain结果如下:

代码语言:javascript
复制
select count(1) from Profiles

explain结果如下:

分析:根据explain的结果:count(*)是直接取class的记录大小,而count(1)会取出所有class下cluster的记录,然后计算大小,所以count(*)的性能肯定比count(1)快很多。但由于本例中Profiles的数量比较小,性能上看不出大的差别,倘若数量大,性能会有明显的差别。可自行验证数据量比较大的点。

5.6.分组查询

统计Id为9的朋友一度和二度朋友的数量。

代码语言:javascript
复制
MATCH   
    {as:profile,class:Profiles,where:(Id = 9)}-HasFriend->{as:friend,while:($depth < 2),where:($depth > 0),depthAlias:da}   
RETURN da,count(*) 
GROUP BY da 
ORDER BY da desc

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

5.7.分页查询

分页查询friend,获取第9页,每页10条记录。

代码语言:javascript
复制
MATCH       
    {as:profile,class:Profiles}   
RETURN profile 
SKIP 90 
LIMIT 100

explain结果如下:

分析:分页查询需要skip和limit一起使用,其思路和mysql的limit分页是一致的。需要查询出前limit条,然后通过skip跳过来分页,当数据量大且查询页数越大时查询性能越慢。使用时请慎重使用,可考虑基于索引限制条件来分页。

5.8.拆分SQL语句

假如有些场景的查询需要一个点与三条以及上的边关联,那么如何写这个SQL呢?按照我们目前理解的MATCH写法一个node只能左边关联一个node,右边关联一个node。这个地方就需要拆分 SQL语句。

找一个客户,这个客户既有吃过某个酒店,也居住过某个酒店,也访问过某些旅游景点。SQL如下:

代码语言:javascript
复制
MATCH     
    {as:customer,class:Customers,where:(Phone = '+1400844724')}.out('HasVisited'),         {as:customer}.out('HasStayed'),        
    {as:customer}.out('HasEaten')    
RETURN distinct customer

5.9.实现LEFT JOIN的功能

查询出所有Customers,要求Customers必须要有Friend,SQL如下:

代码语言:javascript
复制
MATCH     
    {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend}   
RETURN customer,friend

那么如果查询出所有Customers同时带出它们的Friend,即使没有Friend的Customers也要查询出来。那么该如何写呢?

代码语言:javascript
复制
MATCH     
    {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend,optional:true}   RETURN customer,friend

5.10.实现INNER JOIN的功能

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

代码语言:javascript
复制
MATCH    
    {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{as:friend,where:($matched.profile.Name = Name)}   
RETURN distinct customer,profile,friend

分析:借助$matched变量引用另外一个点的别名,然后通过别名访问相关属性。这个示例中的数据显示它自己是自己的朋友,这个仅说明如何使用,不用太关心具体的业务数据。

5.11.已知RID查询

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

代码语言:javascript
复制
MATCH     
   {as:customer,rid:#121:0}-HasProfile->{as:profile}-HasFriend-{as:friend}  
RETURN distinct customer,profile,friend

分析:这个特性orientdb官方并没有暴露出来,虽然目前试验下来orientdb2.x和orientdb3.x都支持,但请慎重使用,说不定下个版本应当不支持了。

5.12.基于边上的条件查询

查询在2018-10-17这个日期成为朋友的用户和朋友。

由于边HasFriend上的属性From和SQL关键字冲突,无法根据此属性查询,所以我们需要新建个属性,执行SQL:

代码语言:javascript
复制
update edge HasFriend set since = '2018-10-17' limit 1

然后执行如下SQL:

代码语言:javascript
复制
MATCH     
   {as:customer,class:Customers}-HasProfile->{as:profile}.bothE('HasFriend'){where:(since = '2018-10-17')}.bothV(){as:friend}   
RETURN distinct customer,profile,friend

5.13.如何避免查询环

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

代码语言:javascript
复制
MATCH
   {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{}-HasFriend-{as:friend2}   
RETURN distinct customer,profile,friend2 limit 10

通过上图查询结果高亮部分我们可以知道用的朋友的朋友是它自己,形成了一个查询环,这样的数据应该剔除,那么如何剔除呢?需要借助两个变量$matched和$currentMatch,$matched前边例子已经介绍过,要想在{}点内访问另外一个{}点,必须借助$matched。不过访问当前{}点也是不能直接使用当前{}点定义的别名的,需要借助$currentMatch。

代码语言:javascript
复制
MATCH
     {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{}-HasFriend-{as:friend2,where:($currentMatch != $matched.profile)}   
RETURN distinct customer,profile,friend2 limit 10

06 练 习

查询用户的二度内(包括二度)朋友的数量。如果一个用户的二度朋友既是一度朋友,那么这个朋友算一度朋友。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-12-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 曲水流觞TechRill 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01 简介
  • 02 MATCH语法格式介绍
  • 03 MATCH的使用
    • 3.1.在browse控制台中使用
      • 3.2.在graph控制台中使用
        • 3.3.使用API
        • 04 编写MATCH语句的规则
          • 4.1.确定查询的起始点
            • 4.2.MATCH中必须要有一个class显示的声明的node
              • 4.3.如果只有一个node声明了class,那么这个node就是起始点,无论这个node有没有过滤条件
                • 4.4.起始点必须要显示声明class
                  • 4.5.起始点的过滤条件尽量加索引
                    • 4.7.尽量声明边的名称和方向
                    • 05 MATCH实战
                      • 5.1.MATCH返回的结果的去重处理
                        • 5.2.MATCH返回的结果的数量
                          • 5.3.1.使用maxDepth进行深度遍历
                          • 5.3.2.使用while和$depth进行深度遍历
                      • 5.3.深度遍历查询
                        • 5.4.RETURN上下文变量的使用
                          • 5.5.使用count(*)而不是count(1)
                            • 5.6.分组查询
                              • 5.7.分页查询
                                • 5.8.拆分SQL语句
                                  • 5.9.实现LEFT JOIN的功能
                                    • 5.10.实现INNER JOIN的功能
                                      • 5.11.已知RID查询
                                        • 5.12.基于边上的条件查询
                                          • 5.13.如何避免查询环
                                          • 06 练 习
                                          相关产品与服务
                                          领券
                                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档