图查询语言GQL(Graph Query Language)语法概览

2024年4月,国际标准化组织(ISO)与国际电工委员会(IEC)共同发布了编号为ISO/IEC 39075:202的图查询语言标准 GQL(Graph Query Language),这是继 ISO 发布第一版 SQL 37年后第二个数据库查询语言标准。目前,国际关联数据基准委员会(Linked Data Benchmark Council,以下简称LDBC)官网上已经发布了GQL相关的语法解析工具和基于antlr的文法文件。预计在GQL的影响下,图厂商会加强对GQL的支持力度,开发者后续进行图分析会更加方便。

笔者发现,目前网上对GQL相关语句的示例比较少,于是读了GQL标准文档,并试着写了一些符合GQL标准语法的语句,与大家分享。希望后面有越来越多的介绍GQL的相关文章,一方面让开发者了解图查询语言,进而了解图可以解决什么问题;另一方面,让图厂商了解GQL标准,推动GQL更快的普及。

注1:本文涉及的所有GQL语句,都在https://opengql.github.io/editor/ 上解析通过。GQL除了语法规则,还存在一部分语义约束,部分语句符合语法规则,但可能会违反语义约束,请读者自行甄别。
注2:本文只是GQL语法功能和应用场景探讨,不涉及某个特性在某个具体图产品上的具体实现讨论。

GQL语法介绍

本文集中对GQL的图管理、数据修改、数据查询、表达式这几部分给出语句示例,涵盖标准的12-20章的内容,每一部分会先尝试描述支持的特性,然后给出语句示例。
下列特性本文只是部分涉及,不做重点描述:

  • Session管理 (第7章)和事务(第8章)
  • 复杂引用类型和字面量(Literal)规则(17章、21章)
  • 语句返回状态和诊断(23章)
  • Binding Variable Definition Block(第9章)

图管理语句

图管理语句描述集中在标准的第12章,GQL提供了对图SCHEMA管理、图管理和图上点边类型管理三部分内容。

SCHEMA管理

SCHEMA管理(GC01)允许一个图数据库实例创建一个或多个SCHEMA,一个SCHEMA下可以创建多个图,不同的SCHEMA实现资源的隔离。
创建一个图SCHEMA,使用下列语句:

CREATE SCHEMA IF NOT EXISTS /catelog/schema_name

其中if not exists是一个可选的语法(GC02)。
删除图SCHEMA,使用下列语句:

DROP SCHEMA IF EXISTS /catelog/schema_name

图管理

GQL同时兼容强类型图(Closed Graph Type,如GES)和弱类型图(Open Graph Type,如Neo4j),因此创图语句也同时支持两种类型的图。
创图-弱类型图

CREATE OR REPLACE GRAPH /catelog/schema_name/graph_name TYPED ANY

或者

CREATE GRAPH /catelog/schema_name/graph_name ::ANY

GQL中支持使用typed关键字或者“::”关键字指定类型。
创图-强类型图

CREATE GRAPH /catelog/schema_name/graph_name :: one_graph_type

或者在创图语句定义类型(GG03):

CREATE GRAPH graph_name TYPED graph {
  (customer:Customer => {id::STRING, name::STRING}),
  (account:Account => {no1 TYPED STRING, acct_type TYPED STRING }),
(customer)-[:HOLDS]->(account),
(account)-[:TRANSFER {amount::INTEGER}]->(account)
}

此外,还支持引用其他图类型(GG04),以及创图时拷贝其他图类型的数据:

CREATE GRAPH graph_name LIKE another_graph AS COPY OF another_graph

其中AS COPY OF another_graph是可选语法项(GG05),表示数据拷贝,而like关键字(GG04)是表示的图上点边类型信息的拷贝。
删除图的逻辑比较简单,示例如下:

DROP GRAPH IF NOT EXISTS /catelog/schema_name/graph_name

其中IF NOT EXISTS是可选语法项(GC05)。

Graph Type管理

在GQL中,对于Closed Graph Type,提供了一套定义点边类型的能力。
这里有三点值得关注:

  • 对点边类型,GQL认为点边的类型不一定等价于点边的Label(GG02),即在某些情况下,类型可以表示为一组label sets和一组属性的定义的集合。
  • GQL对无向边提供了支持(GH02),通过波浪线可以定义某条类型的边是无向边。
  • 不同type下的同名属性,是否支持不同类型(GG26)。

一个典型的定义Graph Type的语句如:

CREATE GRAPH TYPE IF NOT EXISTS one_graph_type AS {
  (account:Account => {no1::STRING, acct_type::STRING }),
  (account)-[:Transfer {amount::INTEGER}]->(account),
  (account)~[:Transfer2 {amount::INTEGER}]~(account)
}

这里Account和Transfer都是Label名。
如果Account和Transfer是类型名,要这么写:

CREATE GRAPH TYPE IF NOT EXISTS one_graph_type AS {
 NODE Account(account{id::STRING, acct_type::STRING }),
 DIRECTED EDGE Transfer{amount::INTEGER} CONNECTING (account -> account),
 UNDIRECTED EDGE Transfer2{amount::INTEGER} CONNECTING (account ~ account)
}

如果图中类型信息包含了一组label信息(如支持可选特性GG02的图),语句估计会这么写:

CREATE GRAPH TYPE IF NOT EXISTS one_graph_type AS {
 NODE Account(account:AccountA&AccountB{id::STRING, acct_type::STRING }),
 DIRECTED EDGE Transfer:Transfer{amount::INTEGER} CONNECTING (account -> account),
 UNDIRECTED EDGE Transfer2:Transfer2{amount::INTEGER} CONNECTING (account ~ account)
}

此外,也支持拷贝其他的graph_type,如:

CREATE GRAPH TYPE one_graph_type AS COPY OF graph_or_external_reference;
CREATE GRAPH TYPE one_graph_type lIKE like_graph;
CREATE OR REPLACE GRAPH TYPE one_graph_type LIKE like_graph;

删除Graph Type语法比较简单:

DROP GRAPH TYPE IF EXISTS /catelog/one_graph_type

数据修改语句

数据修改语句集中在第13章,和Cypher差异不大,这里只给出示例,不做过多描述。

// insert
USE graph_name INSERT (n:SomeLabel{prop1:'value1'})
INSERT (n:Account)-[r:Transfer{amount:10}]->(m:Account)
// set
MATCH (n:LabelA) WHERE element_id(n)='' SET n.prop='value1', n:LabelA, n =  {p1:'v1',p2:'v2'}
// remove
MATCH (n:LabelA) WHERE element_id(n)='' REMOVE n.prop, n:LabelA
// delete
MATCH p=(n)--(m) DELETE n,m
MATCH p=(n)--(m) DELETE nodes(p)

官方文档中,还有delete subQuery的语义,因为之前没有类似的概念参考,所以这里不进行举例。

数据查询语句

在数据查询语句部分(第14章),GQL扩展了较多的能力,将文法文件读下来,感受有几点:

  • 支持语句像电路一样串并联形成执行流水线,支持临时结果集,以及更灵活的子查询逻辑
  • 支持多种路径模式,对查询意图的表达能力更强
  • 支持变长路径(Quantified paths)以及嵌套变长路径查询,对时序图比较友好
  • 支持SELECT关键字,使用SQL风格的文法查询图数据库

在开始介绍之前,首先枚举一下GQL涉及的查询类子句,以及Cypher中类似语义的子句或者关键字,方便了解Cypher的同学快速入门。

GQL子句 Cypher子句 备注
match match 能力增强
optional optional GQL支持optional后跟子查询
filter/where where 能力类似,区别在表达式的不同
let with 能力类似
for unwind GQL支持输出元素下标值
order by order by GQL支持自定义null值排序时优先级
limit/offset limit/offset 能力类似
return return 能力类似, GQL支持显式的group by子句
select GQL独有的SQL风格的子句,语义上支持多图联合查询
union/except/otherwise ... union 能力增强
yield yield 能力类似

语句流水线

graph TD;
Q1-->Q2;
Q1-->Q3;
Q2-->Q4;
Q3-->Q4;

经常使用图查询来进行业务开发的同学可能会有这种需求,需要将同一个语句的结果(如Q1)作为输入同时给多条语句(如Q2和Q3),然后将Q2和Q3的结果进行进一步加工处理,GQL提供了类似的能力,可以将若干语句像电路一样进行串联和并联,形成流水线执行。其中关键字如下:

  • 并联:

    • UNION(GQ03)
    • OTHERWISE(GQ02)
    • EXCEPT(GQ04,GQ05)
    • INTERSECT(GQ07)
  • 串联:

    • NEXT(GQ20)

例如下面的语句中,通过next和except关键字,检索了某个person的好友喜欢评论的帖子,都分布在哪些标签下,其中这些帖子不包含person自己早于$date时间创作的帖子。

MATCH (person)-[:KNOWS]->{2}(friend) WHERE element_id(n)=$personId RETURN person, friend
NEXT YIELD person, friend
MATCH (friend)<-[:HAS_CREATOR]-(comment:Comment)-[:REPLY_OF]->(post:Post) RETURN post
EXCEPT ALL
MATCH (person)<-[:HAS_CREATOR]-(post:Post) WHERE post.creationDate < $date0 RETURN post
MEXT YIELD post
MATCH (post)<-[:HAS_TAG]-(tag)
RETURN tag.name, count(*)

虽然Cypher也能表达相同的语义,但是,要么子查询会写的很复杂,要么受限于语句文法,会有重复遍历或者数据膨胀的问题。
从这一点上看,语句流水线这个特性,能大大缓解一部分Cypher做星型查询或者长链路查询时,由于查询路径上某一类点结果特别多,造成的数据膨胀以及反复遍历的问题。

Match子句

match子句GQL的描述,Neo4j官网已经介绍了很多内容。这里主要围绕四个特性进行介绍:

  • 遍历模式
  • 可变长路径
  • 路径拼接
  • 点边pattern和label过滤

遍历模式
GQL提供了match mode和path mode/search mode两类遍历模式,其中match mode只能写在每个match关键字后,path mode/search mode可以写在每条路径前,或者match语句后接的keep语句中,这里简要列一下文档中的语法规则。

 ::= MATCH 
 ::=  [  ]
 ::= YIELD 
 ::=
    
    | 
 ::=
    [  ] 
     [  ]
     [  ]
 ::=
     [ {   }... ]
 ::=
    [  ] [  ] 

提炼其中关于遍历模式相关的语法规则,简化后大概为:

 ::=
    [  ]
    [  ] [  ]  
    [ {  [  ] [  ]  }... ]
    [ KEEP  ]
    [  ]

其中关于match mode和path pattern prefix,有如下规则:

 ::=
    
    | 
 ::=
    
    | 
 ::=
     [  ]
 ::=
    WALK
    | TRAIL
    | SIMPLE
    | ACYCLIC
 ::=
    
    | 
    | 

关于Match Mode,可以看到起作用范围为整条match子句,有下列两种模式,根据模式名称可以大概猜出其含义:

模式名称 含义 特性编号
DIFFERENT EDGES 要求match后同一条路径中不能含有相同的边 G002
REPEATABLE ELEMENTS 允许路径中出现重复元素 G003

其中Note 220提到了当match mode为DIFFERENT EDGES时,似乎是对Match后跟的path pattern数量有约束,即如果路径中出现selective path pattern,则当match mode为DIFFERENT EDGES时,path pattern list中只能有这一条path pattern。
关于path mode四种模式,其含义在聊聊超级快的图上多跳过滤查询文章中已经提到过,这里不再赘述。
至于path search prefix,可以用来约束查询数量以及是否优先返回最短路。
下面给出这些模式相关的例句:

编号 特性描述 例句
G010 Explicit WALK keyword MATCH p=WALK (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G011 Advanced path modes: TRAIL MATCH p=TRAIL (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G012 Advanced path modes: SIMPLE MATCH p=SIMPLE (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G013 Advanced path modes:ACYCLIC MATCH p=ACYCLIC (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G015 All path search: explicit ALL keyword” MATCH p=ALL WALK (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G016 Any path search MATCH p=ANY 5 TRAIL (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G017 All shortest path search MATCH p=ALL SHORTEST (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G018 Any shortest path search MATCH p=ANY SHORTEST (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G019 Counted shortest path search match p=SHORTEST 5 (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G020 Counted shortest group search match p=SHORTEST 5 GROUP (n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) WHERE element_id(n)='1' RETURN p
G006 Graph pattern KEEP clause: path mode prefix match p=(n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) KEEP WALK WHERE element_id(n)='1' RETURN p
G007 Graph pattern KEEP clause: path search prefix match p=(n)<-[:HAS_CREATOR]-(:Comment)-[:REPLY_OF]->(s:Post) KEEP ANY SHORTEST 5 WHERE element_id(n)='1' RETURN p

这里有一个shortest group的概念(G020),其含义是将不同长度的路径分组,例如SHORTEST 3 GROUP就是将所有最短路、所有次短路以及除了最短路和次短路外的最短路返回,相关概念neo4j也也是的很好,可以参阅Neo4j - Shortest paths

可变长路径
与Cypher不同,GQL提供可不一样的变长路径参数,相关例句Neo4j官网也有解释:

编号 特性名 例句
G035 Quantified paths MATCH (:Station {name: 'Peckham Rye'})-[link:LINK]-(:Station {name: 'Clapham Junction'}){1,3} return count(*)
G036 Quantified edges MATCH (:Station {name: 'Peckham Rye'})-[link:LINK]-{1,3}(:Station {name: 'Clapham Junction'}) return count(*)

这里{1,3}还可以替换成*{3}+?这样其他的表达方式,分别表示不限定跳数、固定跳数,至少1跳,存在性验证这样的场景。
这种改写方式,对时序图会更友好,想象一个列车中转的场景,要查询列车接续中转,可以用下列的写法:

MATCH (:Station)<-[:CALLS_AT]-(d:Stop)
      ((s:Stop)-[:NEXT0]->(f:Stop) WHERE s.time0 > f.time0){1,3}
      (a:Stop)-[:CALLS_AT]->(:Station)
RETURN d.departs AS departureTime, a.arrives AS arrivalTime

这里通过嵌套路径(G038)的where s.time0 > f.time0表达了路径上节点关系之间的强时序约束,可以用于微博转发分析、疫情传播分析等场景。在具体实现时,相关约束可以下推到遍历过程中。如果cypher要表达类似的语义,至少需要在所有路径产生后,使用ListComprehension语义和range枚举下标结合才能完成,且难以下沉到遍历过程中完成。
此外match语句的相关规则中,对无向边匹配也有相关描述,这里举个例子:

MATCH (a:Station)~[:CALLS_AT]~(d:Stop WHERE d.time0 > a.time0) RETURN d
MATCH (a:Station)~[:CALLS_AT]~>(d:Stop WHERE d.time0 > a.time0) RETURN d
MATCH (a:Station)<~[:CALLS_AT]~(d:Stop WHERE d.time0 > a.time0) RETURN d

此外还有些路径简写的例子,笔者没有做深入分析,只写几条通过了parser的示例语句:

编号 特性名 例句
G047 Relaxed topological consistency: concise edge patterns MATCH p=<-[:R]--[:S]- RETURN p
G044 Basic abbreviated edge patterns MATCH (a:Station)->(d:Stop) return d

路径拼接
GQL还支持对路径进行拼接,以及路径中使用|,以及|+|操作符,这里直接给例子:
G030 Path multiset alternation:

MATCH (p:Person)-[:LIVES_IN]->(c:City)|+|(p:Person)-[:LOCATED_IN]->(c:Place) RETURN p

GQ032 Path pattern union:

MATCH (p:Person)-[:LIVES_IN]->(c:City)|(p:Person)-[:LOCATED_IN]->(c:Place) RETURN p

以及查询结果的路径拼接(GE06):

match p=(a)->(b),p1=(b)->(d) return p||p1
match (a)-[r]->(b),(b)-[r1]->(d) return PATH[a,r,b,r1,d]

点边pattern和label过滤
在GQL中,点边的属性过滤支持两种模式:

特性名 例句
element pattern where clause MATCH (a:Station)<-[:CALLS_AT]-(d:Stop where d.time0 > 1) RETURN d
element property specification MATCH (a:Station)<-[:CALLS_AT]-(d:Stop{time0:1}) RETURN d

显然使用where表达过滤,能力会更为强大,当然,相关过滤条件也可以写在后面的where子句中。
此外label支持若干复杂操作,示例如下:

MATCH (a:Station|Stop) return a
MATCH (a:Station&Stop) return a
MATCH (a:(Station&Stop)|OtherLabel) return a
match (a:%) return a

这里%是wildcard label(G074), match (a:%) return a其含义是匹配有label的点,也就是在G074特性启用后,支持判断点上是否有label。

let子句

Let子句(GQ09)与cypher的with子句类似,这里只给一个示例:

MATCH (n) LET gender = n.gender RETURN gender

for子句

for子句与cypher的unwind类似,表示遍历某个可以遍历的结构,与unwind不同的是,for提供了一种类似于python中enumerate函数的用法,可以同时给出元素下标和索引值。
在官方文档中也同步说明,for语句中可以嵌套一个临时表结构,这里试着给出示例:
GQ10 FOR statement: list value support

MATCH (n:LabelA) WHERE n.age < 10 LET v=collect_list(n) FOR s IN v RETURN s 

GQ24 FOR statement: WITH OFFSET

MATCH (n:LabelA) WHERE n.age < 10 LET v=collect_list(n) 
FOR s IN v WITH OFFSET index 
RETURN index, s 

GQ23 FOR statement: binding table support

TABLE userActivity = {
     MATCH (u:User)-[a:ACTION]->()
     RETURN u.id AS userId, a.ts AS timestamps
 }
 FOR u IN userActivity RETURN u.userId, u.timestamps

GQ11 FOR statement: WITH ORDINALITY

TABLE userActivity = {
     MATCH (u:User)-[a:ACTION]->()
     RETURN u.id AS userId, a.ts AS timestamps
 }
 FOR u IN userActivity WITH ORDINALITY index 
 RETURN index,userId, timestamps

排序和分页子句

排序和分页相关语义与Cypher差异不大,GQL支持了排序时定义null的优先级(GA03),这里给出一个示例:

MATCH (n) RETURN n.name AS name ORDER BY name ASC NULLS FIRST, n.date0 DESC NULLS LAST

也同步给一个limit和offset的示例:

MATCH (n) RETURN n SKIP 10 LIMIT 10

return子句

GQL的结果返回子句中,除了支持order by之外,还可以支持group by子句(GQ15),其他和Cypher查边不大,这里试着写一个sql风格的group by:

MATCH (n) LET type0=n.type0, name = n.name RETURN count(*) GROUP BY type0

select子句

在GQL文档中,提供了SQL风格的select子句,但是奇怪的是,select子句是在focused linear query statement语法规则下,而如果GQL没用启用GQ01 Use grpah相关的规则,focused linear query statement不应出现在GQL语句中,也就是说,select子句的特性是一组可选特性,但是select也可以有不显式声明使用某个graph的写法。
另一个奇怪的点在于,GQL提供了临时表的语法,但是select的语法规则中,其后必须跟一个match子句或者是嵌套子查询,而不能用来操作临时表,这就和SQL+Graph的设计理念有一些冲突。
先来看select的具体规则:

 }
     [  ::=
     }... ]
 ]
 ::=
    FROM {  }
 [ {   ::=