Neo4j Cypher其他子句的实用功能
一、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
确保 Customer
和 Product
节点存在(如果不存在则创建),最后创建 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
找到名为 Alice
的 Person
节点。然后使用 WITH
将爱好列表 hobbies
和找到的节点 n
传递给 FOREACH
。FOREACH (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
节点,然后使用 FOREACH
对 targetNames
列表中的每个目标名字执行子查询。在子查询中,找到对应的目标节点并创建 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
)
在这个例子中,FOREACH
对 numbers
列表中的每个数字执行 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
并传递参数 3
和 5
,YIELD 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
拥有的宠物数量。对于没有宠物的 Person
,count(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 CREATE
和 ON 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'})
尝试匹配或创建名为 Alice
的 Person
节点。如果节点是新创建的,ON CREATE SET p.age = 30, p.gender = 'Female'
会设置 age
为 30,gender
为 Female
。如果节点已经存在,ON MATCH SET p.age = p.age + 1
会将 age
属性增加 1。
5.3 MERGE 关系的复杂操作
在处理关系时,MERGE
同样可以结合 ON CREATE
和 ON 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
在这个例子中,创建或匹配 Charlie
到 David
的 FOLLOWS
关系。如果是新创建的关系,设置 since
为当前日期。如果关系已存在且 since
日期早于 2023 年 1 月 1 日,设置 updated
为 true
。
六、WITH 子句的高级用法
6.1 WITH 基础回顾
WITH
子句用于将一个 Cypher 查询的结果传递给下一个子句。它允许我们在查询中进行中间计算、过滤和分组等操作,然后将处理后的结果传递给后续的 MATCH
、CREATE
、RETURN
等子句。
6.2 WITH 进行复杂计算
我们可以使用 WITH
进行复杂的数学或字符串计算,并将结果传递下去。例如,我们有一些 Product
节点,每个节点有 price
和 quantity
属性,我们想计算每个产品的总价值并进行进一步处理。
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
在这个查询中,通过 sum
、avg
、min
和 max
聚合函数对 Transaction
节点的 amount
属性进行统计,并返回相应的结果。
7.4 RETURN 别名与排序
我们可以给 RETURN
的结果设置别名,并根据这些别名进行排序。例如,我们有 Product
节点,每个节点有 name
和 price
属性,我们想按价格降序输出产品名字和价格,并给价格列设置别名。
MATCH (p:Product)
RETURN p.name, p.price AS product_price
ORDER BY product_price DESC
这里,RETURN p.name, p.price AS product_price
给 p.price
设置别名 product_price
,然后 ORDER BY product_price DESC
按 product_price
降序排列输出结果。