OrientDB图遍历SQL之MATCH
- 2019 年 10 月 27 日
- 筆記
本文介绍的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的语法格式如下:
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控制台中使用
MATCH {as:c,class:Customers,where:(Phone='+1400844724')} RETURN c.Phone,c.OrderId

3.2.在graph控制台中使用
在graph中以图的形式显示数据,需要借助$pathElements或者$elements变量。
MATCH {as:c,class:Customers,where:(Phone='+1400844724')} RETURN $pathElements

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 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”。
MATCH {as:customer,where:(Phone='+1400844724')}-HasProfile->{as:profile}-HasFriend-{as:friend} RETURN distinct customer,profile,friend

4.3.如果只有一个node声明了class,那么这个node就是起始点,无论这个node有没有过滤条件
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.起始点的过滤条件尽量加索引
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的结果。
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返回的结果的去重处理
创建两个点,并且在两个点之间创建三条边,创建语句如下:
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语句:
MATCH {as:v1,class:V,where:(name = 'v1')}--{as:v2} RETURN v1,v2
在orientdb3.0.x的执行结果如下。

分析:返回了3条记录,说明没有去重。但是orientdb3.x支持distinct关键字,可通过如下语句去重。
MATCH {as:v1,class:V,where:(name = 'v1')}--{as:v2} RETURN distinct v1,v2
orientdb3.x这种设计更加合理,由使用者来自主选择结果是否需要去重,而在orientdb2.x版本只返回了1条记录,说明是自动去重的。在使用时需要注意,请自行验证。
5.2.MATCH返回的结果的数量
MATCH返回的结果的数量是所有查询路径的数量。也可理解为根据所有起始结点查询的笛卡儿积之和。
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进行深度遍历
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的组合
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进行过滤
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进行深度遍历
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的组合
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进行过滤
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语法描述部分理解下这几个变量不同。限于篇幅,请自行验证结果。
MATCH {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} RETURN $matches
MATCH {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} RETURN $paths
MATCH {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} RETURN $elements
MATCH {as:customer,class:Customers,where:(Phone = '+1400844724')}.outE(){} RETURN $pathElements
5.5.使用count(*)而不是count(1)
在关系型数据库中我们建议使用count(1)统计数量,但在orientdb中我们建议使用count(*)而不是count(1)。具体原因,我们通过explain如下SQL来分析下。
select count(*) from Profiles
explain结果如下:

select count(1) from Profiles
explain结果如下:

分析:根据explain的结果:count(*)是直接取class的记录大小,而count(1)会取出所有class下cluster的记录,然后计算大小,所以count(*)的性能肯定比count(1)快很多。但由于本例中Profiles的数量比较小,性能上看不出大的差别,倘若数量大,性能会有明显的差别。可自行验证数据量比较大的点。
5.6.分组查询
统计Id为9的朋友一度和二度朋友的数量。
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条记录。
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如下:
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如下:
MATCH {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend} RETURN customer,friend
那么如果查询出所有Customers同时带出它们的Friend,即使没有Friend的Customers也要查询出来。那么该如何写呢?
MATCH {as:customer,class:Customers}-HasProfile->{}-HasFriend-{as:friend,optional:true} RETURN customer,friend

5.10.实现INNER JOIN的功能
查询出所有Customers,要求它的Name和它的朋友的Name相同。
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,查询出它的朋友。
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:
update edge HasFriend set since = '2018-10-17' limit 1
然后执行如下SQL:
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的朋友的朋友。
MATCH {as:customer,class:Customers}-HasProfile->{as:profile}-HasFriend-{}-HasFriend-{as:friend2} RETURN distinct customer,profile,friend2 limit 10

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

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