CALL 子查询

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

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

增量更新 Player 的 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. 结果
迭代 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 子查询可以减少操作所需的堆内存量。

导入变量

来自外部作用域的变量必须显式导入到 CALL 子查询的内部作用域中,这可以通过使用变量作用域子句导入 WITH 子句(已弃用)来实现。由于子查询针对每个传入输入行进行评估,因此导入的变量将分配该行中相应的值。

变量作用域子句

变量可以使用作用域子句导入到 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 () {
  RETURN 1 AS 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 子查询之前对结果排序

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

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

UNION 后处理

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

如果结果的不同部分需要以不同方式匹配,并且需要对整个结果进行一些聚合,则需要使用子查询。下面的示例查询结合 UNION ALL 使用 CALL 子查询来确定图中每个 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 是两个 OWES 关系指向 Team B 的聚合结果。

查找每个团队的欠款额
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 子句)执行图更新而不改变行数。

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

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

© . All rights reserved.