MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Neo4j Cypher其他子句的实用功能

2021-06-127.6k 阅读

一、UNWIND 子句

1.1 UNWIND 基础概念

在 Neo4j Cypher 中,UNWIND 子句用于将一个列表拆分成多个独立的行,然后在查询中使用这些拆分后的元素。这在处理复杂数据结构,尤其是列表数据时非常有用。当我们需要对列表中的每个元素执行相同的操作,或者将列表元素与图数据库中的节点和关系进行交互时,UNWIND 子句就派上用场了。

1.2 UNWIND 简单示例

假设我们有一个包含一些名字的列表,我们想为每个名字创建一个新的节点。首先,我们定义一个列表:

WITH ['Alice', 'Bob', 'Charlie'] AS names
UNWIND names AS name
CREATE (:Person {name: name})

在上述代码中,WITH ['Alice', 'Bob', 'Charlie'] AS names 定义了一个包含三个名字的列表 names。然后 UNWIND names AS name 将这个列表拆分成三个独立的行,每个行对应列表中的一个名字,并将其赋值给变量 name。最后,CREATE (:Person {name: name}) 为每个名字创建一个 Person 类型的节点,节点的 name 属性设置为对应的名字。

1.3 UNWIND 与关系创建

UNWIND 子句在创建关系时也非常有用。假设我们有两个列表,一个是客户名字列表,另一个是产品名字列表,我们想要创建客户购买产品的关系。

WITH ['Alice', 'Bob'] AS customers, ['Book', 'Pen'] AS products
UNWIND customers AS customer
UNWIND products AS product
MERGE (c:Customer {name: customer})
MERGE (p:Product {name: product})
CREATE (c)-[:BUYS]->(p)

这里,我们首先定义了客户列表 customers 和产品列表 products。通过两次 UNWIND,我们分别将客户和产品列表拆分成独立的行。然后使用 MERGE 确保 CustomerProduct 节点存在(如果不存在则创建),最后创建 BUYS 关系。

1.4 UNWIND 处理嵌套列表

有时候,我们可能会遇到嵌套列表的情况。例如,我们有一个包含客户及其购买产品列表的嵌套结构。

WITH [
    {customer: 'Alice', products: ['Book', 'Pen']},
    {customer: 'Bob', products: ['Phone']}
] AS data
UNWIND data AS item
UNWIND item.products AS product
MERGE (c:Customer {name: item.customer})
MERGE (p:Product {name: product})
CREATE (c)-[:BUYS]->(p)

在这个例子中,data 是一个嵌套列表,每个元素包含一个客户名字和该客户购买的产品列表。首先,UNWIND data AS item 将外层列表拆分,然后 UNWIND item.products AS product 再将每个客户的产品列表拆分。这样我们就可以创建相应的节点和关系。

二、FOREACH 子句

2.1 FOREACH 基础概念

FOREACH 子句用于对列表中的每个元素执行一个 Cypher 子查询。它与 UNWIND 有些相似,但 FOREACH 更侧重于执行子查询而不是拆分列表供后续查询使用。FOREACH 通常用于对列表元素执行一些副作用操作,比如创建节点、关系或更新属性。

2.2 FOREACH 简单示例

假设我们有一个包含一些属性值的列表,我们想为一个已存在的节点添加这些属性。

MATCH (n:Person {name: 'Alice'})
WITH ['hobby1', 'hobby2', 'hobby3'] AS hobbies, n
FOREACH (hobby IN hobbies | SET n.hobbies = coalesce(n.hobbies, []) + hobby)

在上述代码中,首先通过 MATCH 找到名为 AlicePerson 节点。然后使用 WITH 将爱好列表 hobbies 和找到的节点 n 传递给 FOREACHFOREACH (hobby IN hobbies | SET n.hobbies = coalesce(n.hobbies, []) + hobby)hobbies 列表中的每个爱好执行 SET n.hobbies = coalesce(n.hobbies, []) + hobby 操作。coalesce(n.hobbies, []) 确保 n.hobbies 初始值为一个空列表(如果它还不存在),然后将每个爱好添加到该列表中。

2.3 FOREACH 创建关系示例

我们可以使用 FOREACH 来创建多个关系。假设我们有一个节点,并且有一个与之相关的其他节点名字列表,我们想为这个节点与列表中的每个名字对应的节点创建关系。

MATCH (source:Node {name: 'SourceNode'})
WITH ['Target1', 'Target2', 'Target3'] AS targetNames, source
FOREACH (targetName IN targetNames |
    MATCH (target:Node {name: targetName})
    CREATE (source)-[:RELATED_TO]->(target)
)

这里,先找到 SourceNode 节点,然后使用 FOREACHtargetNames 列表中的每个目标名字执行子查询。在子查询中,找到对应的目标节点并创建 RELATED_TO 关系。

2.4 FOREACH 与条件判断

FOREACH 还可以结合条件判断来执行不同的操作。例如,我们有一个包含数字的列表,对于偶数我们想创建一种类型的节点,对于奇数创建另一种类型的节点。

WITH [1, 2, 3, 4] AS numbers
FOREACH (number IN numbers |
    CASE
        WHEN number % 2 = 0 THEN CREATE (:EvenNumber {value: number})
        ELSE CREATE (:OddNumber {value: number})
    END
)

在这个例子中,FOREACHnumbers 列表中的每个数字执行 CASE 语句。如果数字是偶数,创建 EvenNumber 类型的节点;如果是奇数,创建 OddNumber 类型的节点。

三、CALL 子句

3.1 CALL 基础概念

CALL 子句用于调用 Neo4j 中的存储过程。存储过程是一组预定义的 Cypher 代码,可以重复使用,提供了更强大和灵活的功能。Neo4j 自带了一些系统存储过程,同时用户也可以创建自定义的存储过程。CALL 子句允许我们在 Cypher 查询中执行这些存储过程,并处理其返回结果。

3.2 调用系统存储过程示例

例如,我们可以使用 apoc.export.csv.all 存储过程(APOC 是一组常用的 Neo4j 扩展库中的存储过程集合)将整个图数据库导出为 CSV 文件。

CALL apoc.export.csv.all('path/to/output.csv', {useTypes: true})

在上述代码中,apoc.export.csv.all 是存储过程的名称,'path/to/output.csv' 是输出文件的路径,{useTypes: true} 是传递给存储过程的参数,这里表示在导出的 CSV 文件中包含数据类型信息。

3.3 处理存储过程返回结果

有些存储过程会返回数据,我们可以处理这些返回结果。比如 apoc.meta.data() 存储过程返回图数据库的元数据信息。

CALL apoc.meta.data()
YIELD label, property, type
RETURN label, property, type

这里,CALL apoc.meta.data() 调用存储过程,YIELD label, property, type 定义了存储过程返回的字段名,最后 RETURN label, property, type 将这些返回结果展示出来。

3.4 创建和调用自定义存储过程

我们也可以创建自己的存储过程。首先,我们需要在 Neo4j 的插件目录中创建一个包含存储过程代码的文件(假设使用 Java 编写)。例如,创建一个简单的存储过程来计算两个数的和。

import org.neo4j.procedure.*;

import java.util.Map;

public class CustomProcedures {
    @Procedure
    @Description("com.example.sum(a, b) - Returns the sum of two numbers.")
    public static Result sum(@Name("a") int a, @Name("b") int b) {
        return Results.of(
            Values.parameters("sum", a + b)
        );
    }
}

然后将编译后的代码(.jar 文件)放置在 Neo4j 的插件目录中。在 Cypher 查询中调用这个自定义存储过程:

CALL com.example.sum(3, 5)
YIELD sum
RETURN sum

这里,CALL com.example.sum(3, 5) 调用自定义存储过程 com.example.sum 并传递参数 35YIELD sum 定义返回结果的字段名,最后 RETURN sum 展示计算得到的和。

四、OPTIONAL MATCH 子句

4.1 OPTIONAL MATCH 基础概念

OPTIONAL MATCH 子句与 MATCH 类似,但它不会因为匹配失败而导致整个查询失败。当我们想要查找某些节点或关系,并且即使这些节点或关系不存在也希望查询继续执行并返回部分结果时,OPTIONAL MATCH 就非常有用。它会返回所有匹配的结果以及那些没有匹配到的结果(在没有匹配到的情况下,相关变量会被设置为 null)。

4.2 OPTIONAL MATCH 简单示例

假设我们有 Person 节点和 HAS_PET 关系连接到 Pet 节点,有些 Person 可能没有宠物。我们想查询所有 Person 及其可能拥有的宠物。

MATCH (p:Person)
OPTIONAL MATCH (p)-[:HAS_PET]->(pet:Pet)
RETURN p.name, pet.name

在这个查询中,MATCH (p:Person) 找到所有 Person 节点。然后 OPTIONAL MATCH (p)-[:HAS_PET]->(pet:Pet) 尝试找到每个 Person 拥有的宠物。如果某个 Person 没有宠物,pet 变量会被设置为 null。最后 RETURN p.name, pet.name 返回 Person 的名字和其可能拥有的宠物的名字,如果没有宠物,宠物名字为 null

4.3 OPTIONAL MATCH 与聚合函数

OPTIONAL MATCH 经常与聚合函数一起使用。例如,我们想统计每个 Person 拥有的宠物数量,包括没有宠物的 Person

MATCH (p:Person)
OPTIONAL MATCH (p)-[:HAS_PET]->(pet:Pet)
WITH p, count(pet) AS petCount
RETURN p.name, petCount

这里,OPTIONAL MATCH 后使用 count(pet) 统计每个 Person 拥有的宠物数量。对于没有宠物的 Personcount(pet) 结果为 0。最后返回 Person 的名字和其宠物数量。

4.4 OPTIONAL MATCH 的复杂情况

我们还可以在 OPTIONAL MATCH 中使用更复杂的模式。比如,我们有 Person 节点,Person 可能有工作经历(通过 WORKED_AT 关系连接到 Company 节点),并且 Company 可能有一个 FOUNDED 日期属性。我们想查询所有 Person 及其工作过的公司(如果有)以及公司的成立日期(如果公司有这个属性)。

MATCH (p:Person)
OPTIONAL MATCH (p)-[:WORKED_AT]->(c:Company)
OPTIONAL MATCH (c) WHERE exists(c.FOUNDED)
RETURN p.name, c.name, c.FOUNDED

在这个查询中,第一个 OPTIONAL MATCH 查找 Person 的工作经历,第二个 OPTIONAL MATCH 进一步检查公司是否有 FOUNDED 属性。这样即使某个 Person 没有工作经历,或者某个公司没有 FOUNDED 属性,查询也能正常返回结果。

五、MERGE 子句的更多特性

5.1 MERGE 基础回顾

MERGE 子句用于确保图数据库中存在特定的节点或关系。如果节点或关系不存在,MERGE 会创建它们;如果已经存在,则不会进行重复创建,而是使用已有的节点或关系。其基本语法为 MERGE (node:Label {property: value}) 用于节点,MERGE (node1)-[rel:RelationshipType {property: value}]->(node2) 用于关系。

5.2 MERGE 与 ON CREATE 和 ON MATCH

MERGE 子句可以结合 ON CREATEON MATCH 子句来执行不同的操作。例如,当创建一个新节点时,我们可能想设置一些初始属性,而当匹配到已存在节点时,我们可能想更新某些属性。

MERGE (p:Person {name: 'Alice'})
ON CREATE SET p.age = 30, p.gender = 'Female'
ON MATCH SET p.age = p.age + 1

在上述代码中,MERGE (p:Person {name: 'Alice'}) 尝试匹配或创建名为 AlicePerson 节点。如果节点是新创建的,ON CREATE SET p.age = 30, p.gender = 'Female' 会设置 age 为 30,genderFemale。如果节点已经存在,ON MATCH SET p.age = p.age + 1 会将 age 属性增加 1。

5.3 MERGE 关系的复杂操作

在处理关系时,MERGE 同样可以结合 ON CREATEON MATCH 进行复杂操作。假设我们有 Person 节点和 LIKES 关系连接到 Movie 节点,并且关系有一个 rating 属性表示喜欢程度。

MATCH (p:Person {name: 'Bob'}), (m:Movie {title: 'The Matrix'})
MERGE (p)-[r:LIKES]->(m)
ON CREATE SET r.rating = 5
ON MATCH SET r.rating = r.rating + 1

这里,首先通过 MATCH 找到 Bob 这个人以及 The Matrix 这部电影。然后 MERGE (p)-[r:LIKES]->(m) 尝试匹配或创建 LIKES 关系。如果关系是新创建的,设置初始 rating 为 5;如果关系已存在,将 rating 增加 1。

5.4 MERGE 与条件判断

我们可以在 MERGE 中使用条件判断来决定执行何种操作。例如,我们只想在满足特定条件时更新属性。

MATCH (p:Person {name: 'Charlie'})
MERGE (p)-[r:FOLLOWS]->(f:Person {name: 'David'})
ON CREATE SET r.since = date()
ON MATCH
    WHERE r.since < date({year: 2023, month: 1, day: 1})
    SET r.updated = true

在这个例子中,创建或匹配 CharlieDavidFOLLOWS 关系。如果是新创建的关系,设置 since 为当前日期。如果关系已存在且 since 日期早于 2023 年 1 月 1 日,设置 updatedtrue

六、WITH 子句的高级用法

6.1 WITH 基础回顾

WITH 子句用于将一个 Cypher 查询的结果传递给下一个子句。它允许我们在查询中进行中间计算、过滤和分组等操作,然后将处理后的结果传递给后续的 MATCHCREATERETURN 等子句。

6.2 WITH 进行复杂计算

我们可以使用 WITH 进行复杂的数学或字符串计算,并将结果传递下去。例如,我们有一些 Product 节点,每个节点有 pricequantity 属性,我们想计算每个产品的总价值并进行进一步处理。

MATCH (p:Product)
WITH p, p.price * p.quantity AS totalValue
WHERE totalValue > 100
RETURN p.name, totalValue

在这个查询中,WITH p, p.price * p.quantity AS totalValue 计算每个产品的总价值并命名为 totalValue。然后 WHERE totalValue > 100 过滤出总价值大于 100 的产品。最后返回产品名字和总价值。

6.3 WITH 与分组

WITH 经常与分组函数一起使用。比如,我们有 Person 节点和 HAS_PET 关系连接到 Pet 节点,我们想按宠物种类统计拥有每种宠物的人数。

MATCH (p:Person)-[:HAS_PET]->(pet:Pet)
WITH pet.type, count(p) AS ownerCount
RETURN pet.type, ownerCount

这里,MATCH (p:Person)-[:HAS_PET]->(pet:Pet) 找到所有拥有宠物的人及其宠物。WITH pet.type, count(p) AS ownerCount 按宠物种类 pet.type 分组,并统计每组的人数 ownerCount。最后返回宠物种类和对应的拥有人数。

6.4 WITH 链式操作

WITH 可以进行链式操作,在多个步骤中逐步处理数据。例如,我们有 Employee 节点,每个节点有 salary 属性,我们想先找到工资高于平均工资的员工,然后再按部门统计这些员工的平均工资。

MATCH (e:Employee)
WITH collect(e.salary) AS allSalaries, e
WITH avg(allSalaries) AS averageSalary, e
WHERE e.salary > averageSalary
WITH e.department, collect(e.salary) AS highSalaries
WITH e.department, avg(highSalaries) AS avgHighSalary
RETURN e.department, avgHighSalary

在这个复杂的查询中,首先使用 collect(e.salary) 收集所有员工的工资,然后计算平均工资。接着过滤出工资高于平均工资的员工,再按部门分组并收集这些高工资员工的工资,最后计算每个部门高工资员工的平均工资并返回。

七、RETURN 子句的扩展功能

7.1 RETURN 基础回顾

RETURN 子句用于指定查询结果的输出。它可以返回节点、关系、属性以及通过计算得到的值。基本语法为 RETURN variable1, variable2 或者 RETURN expression1, expression2

7.2 RETURN 格式化输出

我们可以对 RETURN 的输出进行格式化。例如,我们想以特定格式输出日期。假设我们有 Event 节点,每个节点有 start_date 属性为日期类型。

MATCH (e:Event)
RETURN e.name, date.format(e.start_date, 'yyyy - MM - dd') AS formatted_date

这里,date.format(e.start_date, 'yyyy - MM - dd') 使用 date.format 函数将 start_date 格式化为 yyyy - MM - dd 的字符串格式,并命名为 formatted_date 输出。

7.3 RETURN 与聚合函数高级应用

RETURN 可以结合聚合函数进行更高级的统计。例如,我们有 Transaction 节点,每个节点有 amount 属性,我们想统计交易金额的总和、平均值、最小值和最大值。

MATCH (t:Transaction)
RETURN sum(t.amount) AS total_amount, avg(t.amount) AS average_amount, min(t.amount) AS min_amount, max(t.amount) AS max_amount

在这个查询中,通过 sumavgminmax 聚合函数对 Transaction 节点的 amount 属性进行统计,并返回相应的结果。

7.4 RETURN 别名与排序

我们可以给 RETURN 的结果设置别名,并根据这些别名进行排序。例如,我们有 Product 节点,每个节点有 nameprice 属性,我们想按价格降序输出产品名字和价格,并给价格列设置别名。

MATCH (p:Product)
RETURN p.name, p.price AS product_price
ORDER BY product_price DESC

这里,RETURN p.name, p.price AS product_pricep.price 设置别名 product_price,然后 ORDER BY product_price DESCproduct_price 降序排列输出结果。