子句组合
本节描述了 Cypher® 在组合不同读写子句时的语义。
查询由多个链接在一起的子句组成。有关这些子句的更详细讨论,请参阅子句章节。
整个查询的语义由其子句的语义定义。每个子句的输入是图的状态和一个包含当前变量的中间结果表。子句的输出是新的图状态和新的中间结果表,作为下一个子句的输入。第一个子句的输入是查询前的图状态,以及一个初始化为单行空数据的中间结果表。最后一个子句的输出即为查询结果。
|
除非使用了 ORDER BY,否则 Neo4j 不保证查询结果的行顺序。 |
本节全文使用以下示例图。
下面是以下查询在每个子句之后的中间结果表和图状态:
MATCH (john:Person {name: 'John'})
MATCH (john)-[:FRIEND]->(friend)
RETURN friend.name AS friendName
该查询仅包含读子句,因此图的状态保持不变,下文略去图状态。
| 子句 | 子句之后的中间结果表 | ||||||
|---|---|---|---|---|---|---|---|
MATCH (john:Person {name: 'John'})
|
|
||||||
MATCH (john)-[:FRIEND]->(friend) |
|
||||||
RETURN friend.name AS friendName |
|
以上示例仅涉及允许线性组合的子句,省略了写子句。下一节将探讨这些非线性组合和写子句。
读写查询
在 Cypher 查询中,读子句和写子句可以交替执行。读写查询最重要的方面是图的状态也会在子句之间发生变化。
| 子句永远无法观察到后续子句所做的写入,但会观察到之前所有子句完成的写入。 |
自 Cypher 25 起,读子句和写子句可以按任意顺序组合。也就是说,写子句后跟读子句不再需要中间的 WITH 子句,即可让读子句观察到写子句所做的更改。例如,在 Cypher 25 中,下述查询在没有中间 WITH 子句的情况下,由写子句(SET)所做的更改可被随后的 MATCH 子句观察到,这是合法的;但在 Cypher 5 中则不然。
WITH 子句的情况下组合写子句和读子句MATCH (j:Person {name: 'John'})-[:FRIEND]->(f)
SET f.degreesFromJohn = 1
MATCH (f)-[:FRIEND]->(f2)
SET f2.degreesFromJohn = f.degreesFromJohn + 1
RETURN f.name AS friendName,
f.degreesFromJohn AS friendDegree,
f2.name AS friendOfFriendName,
f2.degreesFromJohn AS friendOfFriendDegree
使用与上述相同的示例图,此示例展示了包含“读子句在前,写子句在后”的查询中,每个子句之后的中间结果表和图状态。
MATCH (j:Person) WHERE j.name STARTS WITH "J"
CREATE (j)-[:FRIEND]->(jj:Person {name: "Jay-jay"})
该查询查找 name 属性以 "J" 开头的所有节点,并为每个这样的节点创建一个 name 属性设置为 "Jay-jay" 的新节点。
| 子句 | 子句之后的中间结果表 | 子句后的图状态,红色部分为更改 | ||||||
|---|---|---|---|---|---|---|---|---|
MATCH (j:Person) WHERE j.name STARTS WITH "J" |
|
|||||||
CREATE (j)-[:FRIEND]->(jj:Person {name: "Jay-jay"})
|
|
需要注意的是,MATCH 子句不会找到由 CREATE 子句创建的 Person 节点,即使名称 "Jay-jay" 是以 "J" 开头的。这是因为 CREATE 子句在 MATCH 子句之后,因此 MATCH 无法观察到 CREATE 对图所做的任何更改。
在空图上,此示例展示了包含“写子句在前,读子句在后”的查询中,每个子句之后的中间结果表和图状态。
UNWIND ["Max", "Lune"] AS dogName
CREATE (n:Dog {name: dogName})
WITH n
MATCH (d:Dog)
RETURN COUNT(*)
该查询创建了两个 Dog 节点并返回数值 4。
| 子句 | 子句之后的中间结果表 | 子句后的图状态,红色部分为更改 | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
UNWIND ["Max", "Luna"] AS dogName |
|
||||||||||||||||
CREATE (n:Dog {name: dogName})
|
|
||||||||||||||||
MATCH (d:Dog) |
|
需要注意的是,MATCH 子句读取了由 CREATE 子句创建的所有 Dog 节点。这是因为 CREATE 子句在 MATCH 子句之前,因此 MATCH 可以观察到 CREATE 对图所做的所有更改。MATCH 子句针对每个中间结果执行,这导致两个中间结果分别找到了两个节点。
包含 UNION 的查询
UNION 查询略有不同,因为它们将两个或多个查询的结果合并在一起,但每个查询都从空的中间结果表开始。
在包含 UNION 子句的查询中,UNION *之前*的任何子句都无法观察到 UNION *之后*的子句所做的写入。UNION *之后*的任何子句都可以观察到 UNION *之前*的子句所做的所有写入。这意味着“子句永远无法观察到后续子句所做的写入”这一规则在涉及 UNION 的查询中仍然适用。
UNION 的查询中的中间结果表和图状态使用与上述相同的示例图,此示例展示了以下查询中每个子句之后的中间结果表和图状态。
CREATE (jj:Person {name: "Jay-jay"})
RETURN count(*) AS count
UNION
MATCH (j:Person) WHERE j.name STARTS WITH "J"
RETURN count(*) AS count
| 子句 | 子句之后的中间结果表 | 子句后的图状态,红色部分为更改 | ||||
|---|---|---|---|---|---|---|
CREATE (jj:Person {name: "Jay-jay"})
|
|
|||||
RETURN count(*) AS count |
|
|||||
MATCH (j:Person) WHERE j.name STARTS WITH "J" |
|
|||||
RETURN count(*) AS count |
|
需要注意的是,MATCH 子句找到了由 CREATE 子句创建的 Person 节点。这是因为 CREATE 子句在 MATCH 子句之前,因此 MATCH 可以观察到 CREATE 对图所做的任何更改。
包含 CALL {} 子查询的查询
CALL {} 子句内的子查询会针对每个传入的输入行进行求值。这意味着子查询内的写子句可能会被执行多次。子查询的不同调用会按照传入输入行的顺序依次执行。
子查询的后续调用可以观察到子查询早期调用所做的写入。
CALL {} 的查询中的中间结果表和图状态使用与上述相同的示例图,此示例展示了以下查询中每个子句之后的中间结果表和图状态。
下述查询使用变量作用域子句将变量导入到 CALL 子查询中。 |
MATCH (john:Person {name: 'John'})
SET john.friends = []
WITH john
MATCH (john)-[:FRIEND]->(friend)
WITH john, friend
CALL (john, friend) {
WITH john.friends AS friends
SET john.friends = friends + friend.name
}
| 子句 | 子句之后的中间结果表 | 子句后的图状态,红色部分为更改 | ||||||
|---|---|---|---|---|---|---|---|---|
MATCH (john:Person {name: 'John'})
|
|
|||||||
SET john.friends = [] |
|
|||||||
MATCH (john)-[:FRIEND]->(friend) |
|
|||||||
首次调用 WITH john.friends AS friends |
|
|||||||
首次调用 SET john.friends = friends + friend.name |
|
|||||||
第二次调用 WITH john.friends AS friends |
|
|||||||
第二次调用 SET john.friends = friends + friend.name |
|
需要注意的是,在子查询中,WITH 子句的第二次调用可以观察到 SET 子句的第一次调用所做的写入。
关于实现说明
实现上述语义的一种简单方法是完全执行每个子句,并在执行下一个子句之前将中间结果表物化(materialize)。这种方法会消耗大量内存来物化中间结果表,且性能通常不佳。
相反,Cypher 通常会尝试交错执行子句。这被称为惰性求值 (lazy evaluation)。它仅在需要时才会物化中间结果。在许多读写查询中,交错执行子句是没有问题的,但在有问题的情况下,Cypher 必须确保中间结果表在正确的时间被物化。这是通过在执行计划中插入一个 Eager 操作符来实现的。