子句组合

本节描述了 Cypher® 在组合不同读写子句时的语义。

查询由多个链接在一起的子句组成。有关这些子句的更详细讨论,请参阅子句章节。

整个查询的语义由其子句的语义定义。每个子句的输入是图的状态和一个包含当前变量的中间结果表。子句的输出是新的图状态和新的中间结果表,作为下一个子句的输入。第一个子句的输入是查询前的图状态,以及一个初始化为单行空数据的中间结果表。最后一个子句的输出即为查询结果。

大多数子句会依次作用于中间结果表的每一行,为每一行输入产生零个、一个或多个输出行。此规则的例外情况是投影,例如带有聚合函数的 RETURNWITH,它们会一次性对整个中间结果表进行操作。

除非使用了 ORDER BY,否则 Neo4j 不保证查询结果的行顺序。

示例 1. 读子句之间的中间结果表

本节全文使用以下示例图。

Diagram

下面是以下查询在每个子句之后的中间结果表和图状态:

MATCH (john:Person {name: 'John'})
MATCH (john)-[:FRIEND]->(friend)
RETURN friend.name AS friendName

该查询仅包含读子句,因此图的状态保持不变,下文略去图状态。

每个子句之后的中间结果表
子句 子句之后的中间结果表
MATCH (john:Person {name: 'John'})
john

(:Person {name: 'John'})

MATCH (john)-[:FRIEND]->(friend)
john friend

(:Person {name: 'John'})

(:Person {name: 'Sara'})

(:Person {name: 'John'})

(:Person {name: 'Joe'})

RETURN friend.name AS friendName
friendName

'Sara'

'Joe'

以上示例仅涉及允许线性组合的子句,省略了写子句。下一节将探讨这些非线性组合和写子句。

读写查询

在 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
示例 2. 读子句与写子句之间的中间结果表和图状态

使用与上述相同的示例图,此示例展示了包含“读子句在前,写子句在后”的查询中,每个子句之后的中间结果表和图状态。

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"
j

(:Person {name: 'John'})

(:Person {name: 'Joe'})

Diagram
CREATE (j)-[:FRIEND]->(jj:Person {name: "Jay-jay"})
j jj

(:Person {name: 'John'})

(:Person {name: 'Jay-jay'})

(:Person {name: 'Joe'})

(:Person {name: 'Jay-jay'})

Diagram

需要注意的是,MATCH 子句不会找到由 CREATE 子句创建的 Person 节点,即使名称 "Jay-jay" 是以 "J" 开头的。这是因为 CREATE 子句在 MATCH 子句之后,因此 MATCH 无法观察到 CREATE 对图所做的任何更改。

示例 3. 写子句与读子句之间的中间结果表和图状态

在空图上,此示例展示了包含“写子句在前,读子句在后”的查询中,每个子句之后的中间结果表和图状态。

UNWIND ["Max", "Lune"] AS dogName
CREATE (n:Dog {name: dogName})
WITH n
MATCH (d:Dog)
RETURN COUNT(*)

该查询创建了两个 Dog 节点并返回数值 4

写读:每个子句之后的中间结果表和图状态
子句 子句之后的中间结果表 子句后的图状态,红色部分为更改
UNWIND ["Max", "Luna"] AS dogName
dogName

"Max"

"Luna"

CREATE (n:Dog {name: dogName})
dogName n

"Max"

(:Dog {name: 'Max'})

"Luna"

(:Dog {name: 'Luna'})

Diagram
MATCH (d:Dog)
dogName n d

"Max"

(:Dog {name: 'Max'})

(:Dog {name: 'Max'})

"Max"

(:Dog {name: 'Max'})

(:Dog {name: 'Luna'})

"Luna"

(:Dog {name: 'Luna'})

(:Dog {name: 'Max'})

"Luna"

(:Dog {name: 'Luna'})

(:Dog {name: 'Luna'})

Diagram

需要注意的是,MATCH 子句读取了由 CREATE 子句创建的所有 Dog 节点。这是因为 CREATE 子句在 MATCH 子句之前,因此 MATCH 可以观察到 CREATE 对图所做的所有更改。MATCH 子句针对每个中间结果执行,这导致两个中间结果分别找到了两个节点。

包含 UNION 的查询

UNION 查询略有不同,因为它们将两个或多个查询的结果合并在一起,但每个查询都从空的中间结果表开始。

在包含 UNION 子句的查询中,UNION *之前*的任何子句都无法观察到 UNION *之后*的子句所做的写入。UNION *之后*的任何子句都可以观察到 UNION *之前*的子句所做的所有写入。这意味着“子句永远无法观察到后续子句所做的写入”这一规则在涉及 UNION 的查询中仍然适用。

示例 4. 包含 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"})
jj

(:Person {name: 'Jay-jay'})

Diagram
RETURN count(*) AS count
count

1

Diagram
MATCH (j:Person) WHERE j.name STARTS WITH "J"
j

(:Person {name: 'John'})

(:Person {name: 'Joe'})

(:Person {name: 'Jay-jay'})

Diagram
RETURN count(*) AS count
count

3

Diagram

需要注意的是,MATCH 子句找到了由 CREATE 子句创建的 Person 节点。这是因为 CREATE 子句在 MATCH 子句之前,因此 MATCH 可以观察到 CREATE 对图所做的任何更改。

包含 CALL {} 子查询的查询

CALL {} 子句内的子查询会针对每个传入的输入行进行求值。这意味着子查询内的写子句可能会被执行多次。子查询的不同调用会按照传入输入行的顺序依次执行。

子查询的后续调用可以观察到子查询早期调用所做的写入。

示例 5. 包含 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'})
john

(:Person {name: 'John'})

Diagram
SET john.friends = []
john

(:Person {name: 'John', friends: []})

Diagram
MATCH (john)-[:FRIEND]->(friend)
john friend

(:Person {name: 'John', friends: []})

(:Person {name: 'Sara'})

(:Person {name: 'John', friends: []})

(:Person {name: 'Joe'})

Diagram

首次调用

WITH john.friends AS friends
john friend friends

(:Person {name: 'John', friends: []})

(:Person {name: 'Sara'})

[]

Diagram

首次调用

SET john.friends = friends + friend.name
john friend friends

(:Person {name: 'John', friends: ['Sara']})

(:Person {name: 'Sara'})

[]

Diagram

第二次调用

WITH john.friends AS friends
john friend friends

(:Person {name: 'John', friends: ['Sara']})

(:Person {name: 'Joe'})

['Sara']

Diagram

第二次调用

SET john.friends = friends + friend.name
john friend friends

(:Person {name: 'John', friends: ['Sara', 'Joe']})

(:Person {name: 'Joe'})

['Sara']

Diagram

需要注意的是,在子查询中,WITH 子句的第二次调用可以观察到 SET 子句的第一次调用所做的写入。

关于实现说明

实现上述语义的一种简单方法是完全执行每个子句,并在执行下一个子句之前将中间结果表物化(materialize)。这种方法会消耗大量内存来物化中间结果表,且性能通常不佳。

相反,Cypher 通常会尝试交错执行子句。这被称为惰性求值 (lazy evaluation)。它仅在需要时才会物化中间结果。在许多读写查询中,交错执行子句是没有问题的,但在有问题的情况下,Cypher 必须确保中间结果表在正确的时间被物化。这是通过在执行计划中插入一个 Eager 操作符来实现的。

© . This site is unofficial and not affiliated with Neo4j, Inc.