CALL 子查询

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

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

示例图

以下示例使用了具有以下模式的图

要重建该图,请在空的 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
结果
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
结果
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
结果
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
结果
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
结果
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
结果
totalTeams totalPlayers

3

6

行:1

不带变量作用域子句的 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
GQLSTATUS 错误链

42I28: 错误:语法错误或访问规则违规 - 导入的 WITH 使用无效。导入的 WITH 只能包含对外部变量的直接引用。不允许使用 'WHERE'。

42001:错误:语法错误或访问规则冲突 - 无效语法

解决此限制(对于任何导入的 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
结果
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
结果
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 节点都会被返回,无论它们是否具有任何连接到 TeamPLAYS_FOR 关系。

结果
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 子查询中使用 WHEN,以便在谓词评估为 true 时有条件地执行分支。请注意,不同 WHEN 分支返回的列的名称和数量必须相同。有关详细信息,请参阅 条件查询 → 条件子查询

示例 8. 条件 CALL 子查询

此示例使用条件逻辑,通过执行 WHEN/ELSE 分支根据年龄对球员进行分类,并根据年龄为每个球员设置 "Senior""Junior"ageGroup 属性。

使用条件 WHEN 和 ELSE 为每个团队按年龄分类球员。
MATCH (t:Team)
OPTIONAL MATCH (p:Player)-[:PLAYS_FOR]->(t)
CALL (*) {
  WHEN p.age > 25 THEN {
    SET p.ageGroup = "Senior"
    RETURN p.name AS player, p.ageGroup AS ageGroup
  }
  ELSE {
     SET p.ageGroup = "Junior"
     RETURN p.name AS player, p.ageGroup AS ageGroup
  }
}
RETURN player, ageGroup
结果
player ageGroup

"Player A"

"Junior"

"Player B"

"Junior"

"Player D"

"Senior"

"Player E"

"Junior"

"Player F"

"Senior"

行:5

CALL 子查询的执行顺序

外部作用域的行传递到子查询的顺序是不确定的。如果子查询的结果依赖于这些行的顺序,请在 CALL 子句之前使用 ORDER BY 子句,以确保行的特定处理顺序。

示例 9. 在 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
结果
名称 (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 查询的结果。

示例 10. 在 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
结果
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
结果
team amountOwed

"Team B"

7800

"Team C"

-3300

"Team A"

-4500

行:3

聚合

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

示例 11. 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
结果
playerName team

"Player A"

"Team A"

"Player B"

"Team A"

"Player D"

"Team B"

"Player E"

"Team C"

"Player F"

"Team C"

行:5

示例 12. CALL 子查询与独立聚合

子查询也可以执行独立聚合。下面的示例使用 sum() 函数来统计图中 Team 节点之间欠了多少钱。请注意,Team AowedAmount 是两条指向 Team BOWES 关系的聚合结果。

查找每个团队欠多少钱
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
结果
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(*)
结果
count(*)

6

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

总结

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

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

  • CALL 子查询仅在通过变量作用域子句或导入 WITH 子句(已弃用)显式导入时,才能引用封闭查询中的变量。

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

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

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

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