子句组合

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

一个查询由多个子句链接在一起组成。这些子句将在 子句 章中更详细地讨论。

整个查询的语义由其子句的语义定义。每个子句以图的状态和由当前变量组成的中间结果表作为输入。子句的输出是一个新的图状态和一个新的中间结果表,作为下一个子句的输入。第一个子句以查询前的图状态和一个空的中间结果表作为输入。最后一个子句的输出是查询的结果。

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

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

本节中将使用以下示例图。

Diagram

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

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

此查询只有读子句,因此图状态保持不变,因此在下面省略。

表 1. 每个子句后的中间结果表
子句 子句后的中间结果表
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 查询中,读写子句可以轮流执行。读写查询最重要的方面是图的状态也会在子句之间发生变化。

子句永远无法观察到后面子句所做的写操作。

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

使用与上面相同的示例图,此示例显示以下查询每个子句后的中间结果表和图状态:

MATCH (j:Person) WHERE j.name STARTS WITH "J"
CREATE (j)-[:FRIEND]->(jj:Person {name: "Jay-jay"})

此查询找到所有 name 属性以 "J" 开头的节点,并为每个这样的节点创建一个 name 属性设置为 "Jay-jay" 的新节点。

表 2. 每个子句后的中间结果表和图状态
子句 子句后的中间结果表 子句后的图状态,变化以红色显示
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 所做的对图的任何更改。

使用 UNION 的查询

UNION 查询略有不同,因为两个或多个查询的结果被组合在一起,但每个查询都以一个空的中间结果表开始。

在使用 UNION 子句的查询中,UNION 之前的任何子句都无法观察到 UNION 之后的子句所做的写操作。UNION 之后的任何子句都可以观察到 UNION 之前的子句所做的所有写操作。这意味着子句永远无法观察到后面子句所做的写操作的规则,在使用 UNION 的查询中仍然适用。

示例 3. 使用 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
表 3. 每个子句后的中间结果表和图状态
子句 子句后的中间结果表 子句后的图状态,变化以红色显示
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 {} 子句中的子查询是针对每个传入的输入行进行评估的。这意味着子查询中的写子句可能会执行多次。子查询的不同调用按顺序依次执行,即按照传入输入行的顺序执行。

子查询的后续调用可以观察到子查询之前调用所做的写操作。

示例 4. 使用 CALL {} 的查询中的中间结果表和图状态

使用与上面相同的示例图,此示例显示以下查询每个子句后的中间结果表和图状态:

以下查询使用 变量作用域子句(在 Neo4j 5.23 中引入)将变量导入 CALL 子查询。如果您使用的是旧版本的 Neo4j,请改用 导入 WITH 子句
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
}
表 4. 每个子句后的中间结果表和图的状态
子句 子句后的中间结果表 子句后的图状态,变化以红色显示
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 子句的第一次调用所做的写入操作。

关于实现的说明

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

相反,Cypher 通常会尝试交错执行子句。这被称为 延迟评估。它仅在需要时才物化中间结果。在许多读写查询中,交错执行子句是没有问题的,但当它不适合时,Cypher 必须确保中间结果表在适当的时间物化。这是通过在执行计划中插入一个 Eager 运算符来完成的。