子句构成

本节描述了 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 操作符来完成的。