调用子查询

CALL 子句可用于调用在定义范围内执行操作的子查询,从而优化数据处理和查询效率。与 Cypher® 中的其他子查询不同,CALL 子查询可用于对数据库进行更改(例如,创建 新节点)。

CALL 子句还用于调用过程。有关此上下文中CALL 子句的说明,请参阅CALL 过程

示例图

以下示例使用具有以下架构的图

call subquery graph

要重新创建该图,请在空的 Neo4j 数据库中运行以下查询

CREATE (teamA:Team {name: 'Team A'}),
       (teamB:Team {name: 'Team B'}),
       (teamC:Team {name: 'Team C'}),
       (playerA:Player {name: 'Player A', age: 21}),
       (playerB:Player {name: 'Player B', age: 23}),
       (playerC:Player {name: 'Player C', age: 19}),
       (playerD:Player {name: 'Player D', age: 30}),
       (playerE:Player {name: 'Player E', age: 25}),
       (playerF:Player {name: 'Player F', age: 35}),
       (playerA)-[:PLAYS_FOR]->(teamA),
       (playerB)-[:PLAYS_FOR]->(teamA),
       (playerD)-[:PLAYS_FOR]->(teamB),
       (playerE)-[:PLAYS_FOR]->(teamC),
       (playerF)-[:PLAYS_FOR]->(teamC),
       (teamA)-[:OWES {dollars: 1500}]->(teamB),
       (teamA)-[:OWES {dollars: 3000}]->(teamB),
       (teamB)-[:OWES {dollars: 1700}]->(teamC),
       (teamC)-[:OWES {dollars: 5000}]->(teamB)

语义和性能

对于每个传入行,CALL 子查询都会执行一次。子查询中返回的变量可用于包含查询的外部范围。

示例 1. 基本示例

在此示例中,CALL 子查询执行三次,每次针对UNWIND 子句输出的每一行。

查询
UNWIND [0, 1, 2] AS x
CALL () {
  RETURN 'hello' AS innerReturn
}
RETURN innerReturn
表 1. 结果
innerReturn

'hello'

'hello'

'hello'

行:3

CALL 子查询的每次执行都可以观察到先前执行的更改。这允许在单个 Cypher 查询中累积结果和逐步转换数据。

示例 2. 增量更新

在此示例中,CALL 子查询的每次迭代都会将Player Aage 加 1,返回的newAge 反映每次增量后的age

增量更新球员的年龄属性
UNWIND [1, 2, 3] AS x
CALL () {
    MATCH (p:Player {name: 'Player A'})
    SET p.age = p.age + 1
    RETURN p.age AS newAge
}
MATCH (p:Player {name: 'Player A'})
RETURN x AS iteration, newAge, p.age AS totalAge
表 2. 结果
iteration newAge totalAge

1

22

24

2

23

24

3

24

24

行:3

CALL 子查询的范围效果意味着,在每次执行的每一行结束时,在继续执行下一行之前,都可以清理在每次执行期间完成的工作。这允许有效地管理资源,并通过确保在子查询执行期间创建的临时数据结构不会超出其有用性而减少内存开销。因此,CALL 子查询可以帮助保持最佳性能和可扩展性,尤其是在复杂或大规模查询中。

示例 3. 性能

在此示例中,CALL 子查询用于collect 包含所有为特定球队效力的球员的LIST

收集为特定球队效力的所有球员的列表
MATCH (t:Team)
CALL (t) {
  MATCH (p:Player)-[:PLAYS_FOR]->(t)
  RETURN collect(p) as players
}
RETURN t AS team, players
表 3. 结果
team players

(:Team {name: "Team A"})

(:Player {name: "Player B", age: 23}), (:Player {name: "Player A", age: 24})]

(:Team {name: "Team B"})

[(:Player {name: "Player D", age: 30})]

(:Team {name: "Team C"})

[(:Player {name: "Player F", age: 35}), (:Player {name: "Player E", age: 25})]

行:3

CALL 子查询确保每个Team 都单独处理(每个Team 节点一行),而不是在将它们收集到列表之前必须同时将每个TeamPlayer 节点保存在堆内存中。因此,使用CALL 子查询可以减少操作所需的堆内存量。

导入变量

必须通过使用变量范围子句导入WITH 子句(已弃用)来显式地将外部范围的变量导入CALL 子查询的内部范围。由于子查询针对每个传入输入行进行评估,因此导入的变量将被分配来自该行的相应值。

变量范围子句

可以使用范围子句将变量导入CALL 子查询中:CALL (<variable>)。使用范围子句将禁用已弃用的导入WITH 子句。

范围子句可用于导入外部范围的所有、特定或任何变量。

示例 4. 从外部范围导入特定变量

此示例仅从外部范围导入p 变量,并使用它为每个Player 节点创建一个新的随机生成的rating 属性。然后,它返回具有最高ratingPlayer 节点。

从外部范围导入一个变量
MATCH (p:Player), (t:Team)
CALL (p) {
  WITH rand() AS random
  SET p.rating = random
  RETURN p.name AS playerName, p.rating AS rating
}
RETURN playerName, rating, t AS team
ORDER BY rating
LIMIT 1
表 4. 结果
playerName rating team

"Player C"

0.9307432039870395

"Team A"

行:1

要导入其他变量,请在CALL 后面的括号中包含它们,并用逗号分隔。例如,要从上面查询的MATCH 子句中导入两个变量,请相应地修改范围子句:CALL (p, t)

示例 5. 导入所有变量

要导入外部范围的所有变量,请使用CALL (*)。此示例导入pt 变量,并在两者上设置一个新的lastUpdated 属性。

从外部范围导入所有变量
MATCH (p:Player), (t:Team)
CALL (*) {
  SET p.lastUpdated = timestamp()
  SET t.lastUpdated = timestamp()
}
RETURN p.name AS playerName,
       p.lastUpdated AS playerUpdated,
       t.name AS teamName,
       t.lastUpdated AS teamUpdated
LIMIT 1
表 5. 结果
playerName playerUpdated teamName teamUpdated

"Player A"

1719304206653

"Team A"

1719304206653

行:1

示例 6. 不导入任何变量

要从外部范围不导入任何变量,请使用CALL ()

从外部范围不导入任何变量
MATCH (t:Team)
CALL () {
  MATCH (p:Player)
  RETURN count(p) AS totalPlayers
}
RETURN count(t) AS totalTeams, totalPlayers
表 6. 结果
totalTeams totalPlayers

3

6

行:1

从 Neo4j 5.23 开始,使用没有变量范围子句的CALL 子查询已被弃用。

已弃用
MATCH (t:Team)
CALL {
  MATCH (p:Player)
  RETURN count(p) AS totalPlayers
}
RETURN count(t) AS totalTeams, totalPlayers

规则

  • 作用域子句的变量可以在子查询中全局引用。子查询中的后续WITH无法删除导入的变量。已弃用的导入WITH子句的行为不同,因为导入的变量只能从第一行引用,并且可以被后续子句删除。

  • 变量不能在作用域子句中别名化。只允许简单变量引用。

不允许
MATCH (t:Team)
CALL (t AS teams) {
  MATCH (p:Player)-[:PLAYS_FOR]->(teams)
  RETURN collect(p) as players
}
RETURN t AS teams, players
  • 作用域子句的变量不能在子查询中重新声明。

不允许
MATCH (t:Team)
CALL (t) {
  WITH 'New team' AS t
  MATCH (p:Player)-[:PLAYS_FOR]->(t)
  RETURN collect(p) as players
}
RETURN t AS team, players
  • 子查询不能返回外部作用域中已存在的变量名。要返回导入的变量,必须重命名它们。

不允许
MATCH (t:Team)
CALL (t) {
  RETURN t
}
RETURN t

导入WITH子句

变量也可以使用导入的WITH子句导入到CALL子查询中。请注意,此语法不是GQL 兼容的

使用WITH子句导入的变量
MATCH (t:Team)
CALL {
  WITH t
  MATCH (p:Player)-[:PLAYS_FOR]->(t)
  RETURN collect(p) as players
}
RETURN t AS teams, players
点击阅读更多关于使用WITH子句导入变量的信息
  • 与使用变量作用域子句类似,使用导入WITH子句的子查询不能返回外部作用域中已存在的变量名。要返回导入的变量,必须重命名它们。

  • 导入的WITH子句必须是子查询的第一个子句(或者如果直接跟随USE子句,则为第二个子句)。

  • 在导入的WITH子句之后,不能使用以下任何子句:DISTINCTORDER BYWHERESKIPLIMIT

尝试任何上述操作都会抛出错误。例如,以下在导入的WITH子句之后使用WHERE子句的查询将抛出错误

不允许
UNWIND [[1,2],[1,2,3,4],[1,2,3,4,5]] AS l
CALL {
  WITH l
  WHERE size(l) > 2
  RETURN l AS largeLists
}
RETURN largeLists
错误信息
Importing WITH should consist only of simple references to outside variables.
WHERE is not allowed.

针对此限制的解决方案(对导入WITH子句进行任何过滤或排序所必需的)是在导入的WITH子句之后声明第二个WITH子句。第二个WITH子句将充当常规的WITH子句。例如,以下查询不会抛出错误

允许
UNWIND [[1,2],[1,2,3,4],[1,2,3,4,5]] AS l
CALL {
  WITH l
	WITH l
	WHERE size(l) > 2
  RETURN l AS largeLists
}
RETURN largeLists
表 7. 结果
largeLists

[1, 2, 3, 4]

[1, 2, 3, 4, 5]

行数:2

可选子查询调用

OPTIONAL CALL允许可选地执行CALL子查询。类似于OPTIONAL MATCHOPTIONAL CALL子查询产生的任何空行都将返回null

示例 7. 使用CALLOPTIONAL CALL之间的区别

此示例(查找每个Player所属的团队)突出了使用CALLOPTIONAL CALL之间的区别。

常规子查询CALL
MATCH (p:Player)
CALL (p) {
    MATCH (p)-[:PLAYS_FOR]->(team:Team)
    RETURN team
}
RETURN p.name AS playerName, team.name AS team
表 8. 结果
playerName team

"Player A"

"Team A"

"Player B"

"Team A"

"Player D"

"Team B"

"Player E"

"Team C"

"Player F"

"Team C"

行数:5

请注意,没有为Player C返回任何结果,因为他们没有连接到任何与PLAYS_FOR关系相关的Team

使用常规OPTIONAL CALL的查询
MATCH (p:Player)
OPTIONAL CALL (p) {
    MATCH (p)-[:PLAYS_FOR]->(team:Team)
    RETURN team
}
RETURN p.name AS playerName, team.name AS team

现在所有Player节点都将返回,无论它们是否具有连接到Team的任何PLAYS_FOR关系。

表 9. 结果
playerName team

"Player A"

"Team A"

"Player B"

"Team A"

"Player C"

NULL

"Player D"

"Team B"

"Player E"

"Team C"

"Player F"

"Team C"

行数:6

CALL子查询的执行顺序

从外部作用域传递到子查询中的行的顺序没有定义。如果子查询的结果依赖于这些行的顺序,请在CALL子句之前使用ORDER BY子句来保证行的特定处理顺序。

示例 8. 在CALL子查询之前排序结果

此示例创建了所有Player节点的链接列表,按age升序排列。

CALL子句依赖于传入行排序以确保创建正确排序的链接列表,因此传入行必须使用前面的ORDER BY子句进行排序。

CALL子查询之前排序结果
MATCH (player:Player)
WITH player
ORDER BY player.age ASC LIMIT 1
  SET player:ListHead
WITH *
MATCH (nextPlayer: Player&!ListHead)
WITH nextPlayer
ORDER BY nextPlayer.age
CALL (nextPlayer) {
  MATCH (current:ListHead)
    REMOVE current:ListHead
    SET nextPlayer:ListHead
    CREATE(current)-[:IS_YOUNGER_THAN]->(nextPlayer)
  RETURN current AS from, nextPlayer AS to
}
RETURN
  from.name AS name,
  from.age AS age,
  to.name AS closestOlderName,
  to.age AS closestOlderAge
表 10. 结果
name age closestOlderName closestOlderAge

"Player C"

19

"Player B"

23

"Player B"

23

"Player A"

24

"Player A"

24

"Player E"

25

"Player E"

25

"Player D"

30

"Player D"

30

"Player F"

35

行数:5

联合后处理

CALL子查询可用于进一步处理UNION查询的结果。

示例 9. 在CALL子查询中使用UNION

此示例查询查找图中年龄最小和最大的Player

查找年龄最大和最小的玩家
CALL () {
  MATCH (p:Player)
  RETURN p
  ORDER BY p.age ASC
  LIMIT 1
UNION
  MATCH (p:Player)
  RETURN p
  ORDER BY p.age DESC
  LIMIT 1
}
RETURN p.name AS playerName, p.age AS age
表 11. 结果
playerName age

"Player C"

19

"Player F"

35

行数:2

如果结果的不同部分应该以不同的方式匹配,并且对整个结果进行一些聚合,则需要使用子查询。以下示例查询使用CALL子查询结合UNION ALL来确定图中每个Team欠款或应收款的金额。

查找每个团队应收的金额
MATCH (t:Team)
CALL (t) {
  OPTIONAL MATCH (t)-[o:OWES]->(other:Team)
  RETURN o.dollars * -1 AS moneyOwed
  UNION ALL
  OPTIONAL MATCH (other)-[o:OWES]->(t)
  RETURN o.dollars AS moneyOwed
}
RETURN t.name AS team, sum(moneyOwed) AS amountOwed
ORDER BY amountOwed DESC
表 12. 结果
team amountOwed

"Team B"

7800

"Team C"

-3300

"Team A"

-4500

行:3

聚合

返回的子查询会改变查询的结果数量。CALL子查询的结果是对每个输入行评估子查询的组合结果。

示例 10. CALL子查询改变外部查询返回的行数

以下示例查找每个Player的姓名及其所属的团队。没有为Player C返回任何行,因为他们没有连接到任何与PLAYS_FOR关系相关的Team。因此,子查询的结果数量改变了封闭查询的结果数量。

查找玩家的朋友
MATCH (p:Player)
CALL (p) {
  MATCH (p)-[:PLAYS_FOR]->(team:Team)
  RETURN team.name AS team
}
RETURN p.name AS playerName, team
表 13. 结果
playerName team

"Player A"

"Team A"

"Player B"

"Team A"

"Player D"

"Team B"

"Player E"

"Team C"

"Player F"

"Team C"

行数:5

示例 11. CALL子查询和孤立的聚合

子查询还可以执行孤立的聚合。以下示例使用sum()函数来计算图中Team节点之间欠款的总金额。请注意,Team AowedAmount是与Team B的两个OWES关系的聚合结果。

查找每个团队欠款的金额
MATCH (t:Team)
CALL (t) {
  MATCH (t)-[o:OWES]->(t2:Team)
  RETURN sum(o.dollars) AS owedAmount, t2.name AS owedTeam
}
RETURN t.name AS owingTeam, owedAmount, owedTeam
表 14. 结果
owingTeam owedAmount owedTeam

"Team A"

4500

"Team B"

"Team B"

1700

"Team C"

"Team C"

5000

"Team B"

行数:4

关于返回子查询和单元子查询的说明

上面的示例都使用了以RETURN子句结尾的子查询。这些子查询称为返回子查询

子查询针对每个传入的输入行进行评估。返回子查询的每个输出行都与输入行组合在一起,以构建子查询的结果。这意味着返回子查询将影响行数。如果子查询不返回任何行,则子查询之后将没有可用行。

没有RETURN语句的子查询称为单元子查询。单元子查询用于它们使用诸如CREATEMERGESETDELETE之类的子句更改图的能力。它们没有显式返回任何内容,这意味着子查询之后存在的行数与进入子查询的行数相同。

单元子查询

单元子查询用于它们使用更新子句更改图的能力。它们不会影响封闭查询返回的行数。

此示例查询创建图中每个现有Player节点的 3 个克隆。由于子查询是单元子查询,因此它不会改变封闭查询的行数。

创建克隆节点
MATCH (p:Player)
CALL (p) {
  UNWIND range (1, 3) AS i
  CREATE (:Person {name: p.name})
}
RETURN count(*)
表 15. 结果
count(*)

6

行:1
创建的节点:18
设置的属性:18
添加的标签:18

总结

  • CALL子查询优化数据处理和查询效率,并且可以对数据库进行更改。

  • CALL子查询允许逐行数据转换,并能够跨多行累积结果,从而促进依赖于中间数据或聚合数据的复杂操作。

  • CALL子查询只能引用封闭查询中的变量,前提是它们通过变量作用域子句或导入的WITH子句(已弃用)显式导入。

  • CALL子查询返回的所有变量之后都可以在封闭查询中使用。

  • 返回子查询(带有RETURN子句)会影响输出行的数量,而单元子查询(没有RETURN子句)会执行图更新而不改变行数。

  • 可以在CALL子查询之前使用ORDER BY子句来确保特定顺序。

  • CALL子查询可以与UNION结合使用来处理和聚合查询结果的不同部分。