Cypher 查询语言基础
Cypher 是 Neo4j 的声明式查询语言,专为图数据操作而设计。它的设计理念是让复杂的图查询变得简单直观,语法风格类似于自然语言,使得即使是初学者也能快速上手。Cypher 的设计灵感部分来自 SQL,但它针对图数据的特点进行了专门优化。
Cypher 的设计哲学
在学习具体语法之前,理解 Cypher 的设计哲学有助于我们更好地使用它:
声明式而非命令式:Cypher 是声明式语言,你只需要描述"想要什么数据",而不需要告诉数据库"如何获取数据"。查询优化器会自动选择最高效的执行路径。
ASCII 艺术风格:Cypher 使用类似 ASCII 艺术的语法来表示图结构,这使得查询语句具有自描述性。例如 (a)-[:KNOWS]->(b) 直观地表示了节点 a 通过 KNOWS 关系指向节点 b。
模式匹配思想:Cypher 查询的核心是模式匹配——你在查询中描述一个图模式,数据库会在实际图中寻找匹配该模式的数据。
基本语法元素
Cypher 使用一组简洁的符号来表示图的各个组成部分:
节点表示
节点使用圆括号 () 表示,这是 Cypher 中最基础的语法元素:
() -- 任意节点,不关心其标签和属性
(p) -- 节点变量 p,可以在后续查询中引用
(:Person) -- 标签为 Person 的节点
(p:Person) -- 变量为 p,标签为 Person 的节点
(p {name: '张三'}) -- 匹配 name 属性为 '张三' 的节点
(p:Person {name: '张三', age: 30}) -- 完整的节点模式
理解节点变量:变量是可选的,但如果你需要在查询的其他部分引用这个节点,就必须给它一个变量名。变量名只在当前查询语句内有效。
标签的作用:标签类似于关系型数据库中的表名,用于对节点进行分类。一个节点可以有多个标签,如 (p:Person:Employee)。
关系表示
关系使用方括号和箭头 -[ ]-> 表示,箭头指示方向:
--> -- 任意方向的关系,指向右侧
<-- -- 任意方向的关系,指向左侧
-[r]-> -- 关系变量为 r
-[:FRIEND]-> -- 类型为 FRIEND 的关系
-[r:FRIEND]-> -- 变量 r,类型为 FRIEND
-[r:FRIEND {since: '2020'}]-> -- 带属性的关系
方向的重要性:关系是有方向的,但查询时可以忽略方向。使用 -- 表示无向关系(匹配两个方向),使用 --> 或 <-- 表示有向关系。
关系类型:每条关系必须有一个类型,如 FRIEND、WORKS_FOR 等。建议使用大写字母和下划线命名。
完整的模式语法
将节点和关系组合起来,就形成了完整的图模式:
-- 基本模式:a 指向 b
(a)-[r]->(b)
-- 带类型的模式
(a:Person)-[:KNOWS]->(b:Person)
-- 多跳关系(固定深度)
(a)-[:KNOWS]->()-[:KNOWS]->(b) -- a 的朋友的朋友
-- 可变深度关系
(a)-[:KNOWS*2]->(b) -- 恰好 2 跳
(a)-[:KNOWS*1..3]->(b) -- 1 到 3 跳
(a)-[:KNOWS*]->(b) -- 任意深度(谨慎使用)
-- 多种关系类型
(a)-[:KNOWS|WORKS_WITH]->(b) -- KNOWS 或 WORKS_WITH
创建数据(CREATE)
CREATE 语句用于在图中创建新的节点和关系。理解 CREATE 的执行方式很重要:它会无条件创建数据,即使相同的数据已经存在。
创建节点
-- 创建单个节点
CREATE (p:Person {name: '张三', age: 30, city: '北京'})
-- 创建多个节点(一条语句)
CREATE (a:Person {name: '李四', age: 28}),
(b:Person {name: '王五', age: 32})
-- 创建带有多个标签的节点
CREATE (n:Person:Employee:Manager {name: '赵六', department: '技术部'})
创建后返回:CREATE 语句可以和 RETURN 一起使用,查看创建的数据:
CREATE (p:Person {name: '张三', age: 30})
RETURN p.name, p.age
创建关系
关系必须连接两个已存在的节点,或者在创建节点的同时创建关系:
-- 创建节点的同时建立关系
CREATE (a:Person {name: '张三'})-[:FRIEND {since: '2020-01-01'}]->(b:Person {name: '李四'})
-- 为已存在的节点创建关系
MATCH (a:Person {name: '张三'}), (b:Person {name: '王五'})
CREATE (a)-[:COLLEAGUE {department: '技术部'}]->(b)
关系属性:关系可以像节点一样拥有属性,常用于存储关系的元信息,如建立时间、权重等。
批量创建
使用 UNWIND 可以批量创建数据,这在数据导入场景中非常实用:
-- 批量创建节点
UNWIND ['张三', '李四', '王五', '赵六'] AS name
CREATE (p:Person {name: name})
-- 从列表数据批量创建
UNWIND [
{name: '张三', age: 30},
{name: '李四', age: 28},
{name: '王五', age: 32}
] AS data
CREATE (p:Person {name: data.name, age: data.age})
MERGE:创建或匹配
MERGE 是一个强大的语句,它结合了 MATCH 和 CREATE 的功能:如果数据存在则匹配,不存在则创建。这在处理幂等操作时非常有用。
基本 MERGE
-- 如果 Person 节点不存在则创建
MERGE (p:Person {name: '张三'})
RETURN p
-- MERGE 整个模式
MERGE (a:Person {name: '张三'})-[:KNOWS]->(b:Person {name: '李四'})
MERGE 的原子性:MERGE 操作是原子的——要么整个模式匹配成功,要么整个模式被创建。如果模式中只有部分匹配,不会创建部分数据。
ON CREATE 和 ON MATCH
MERGE 提供了两个子句来处理创建和匹配两种情况:
MERGE (p:Person {name: '张三'})
ON CREATE SET
p.created = datetime(),
p.status = 'new'
ON MATCH SET
p.lastSeen = datetime(),
p.visitCount = coalesce(p.visitCount, 0) + 1
RETURN p
典型应用场景:记录数据的创建时间,或者统计数据的访问次数。ON CREATE 只在节点被创建时执行,ON MATCH 只在节点已存在时执行。
MERGE 与唯一性
MERGE 依赖于唯一性约束来确定节点是否存在。如果没有唯一性约束,MERGE 会查找完全匹配的节点(包括所有属性),这可能不是你想要的行为:
-- 如果有唯一性约束,只需要指定关键属性
CREATE CONSTRAINT person_name_unique FOR (p:Person) REQUIRE p.name IS UNIQUE
MERGE (p:Person {name: '张三'}) -- 会利用索引快速查找
ON CREATE SET p.age = 30
查询数据(MATCH)
MATCH 是 Cypher 中最核心的语句,用于在图中查找符合特定模式的数据。
基本查询
-- 查询所有节点(慎用,数据量大时会很慢)
MATCH (n)
RETURN n
-- 查询特定标签的节点
MATCH (p:Person)
RETURN p.name, p.age
-- 查询特定属性
MATCH (p:Person {name: '张三'})
RETURN p
-- 返回特定属性,使用 AS 设置别名
MATCH (p:Person {name: '张三'})
RETURN p.name AS 姓名, p.age AS 年龄
查询优化提示:尽量在 MATCH 中指定标签和属性,这样 Neo4j 可以利用索引加速查询。避免无约束的全表扫描。
查询关系
关系查询是图数据库的核心能力:
-- 查询特定节点的所有关系
MATCH (p:Person {name: '张三'})-[r]->(other)
RETURN type(r) AS 关系类型, other.name AS 相关节点
-- 查询特定类型的关系
MATCH (p:Person {name: '张三'})-[:FRIEND]->(friend)
RETURN friend.name
-- 忽略关系方向
MATCH (p:Person {name: '张三'})-[:FRIEND]-(friend)
RETURN friend.name
-- 查询双向关系
MATCH (p:Person {name: '张三'})-[:FRIEND]-(friend)
WHERE friend.name <> '张三'
RETURN DISTINCT friend.name
方向的重要性:
(a)-[r]->(b)查询从 a 到 b 的关系(a)<-[r]-(b)查询从 b 到 a 的关系(a)-[r]-(b)查询两个方向的关系
多跳查询
多跳查询是图数据库的强项,可以轻松查询"朋友的朋友"这类关系:
-- 查询朋友的朋友(恰好 2 跳)
MATCH (p:Person {name: '张三'})-[:FRIEND]->()-[:FRIEND]->(fof)
RETURN DISTINCT fof.name
-- 可变深度查询(1 到 3 跳)
MATCH (p:Person {name: '张三'})-[:FRIEND*1..3]-(connection)
RETURN DISTINCT connection.name, length(shortestPath((p)-[*]-(connection)))
-- 任意深度查询(谨慎使用,可能导致性能问题)
MATCH (p:Person {name: '张三'})-[:FRIEND*]-(connection)
RETURN DISTINCT connection.name
性能警告:无限制的可变深度查询 (*) 可能遍历整个图,对大图非常危险。始终设置合理的深度上限。
WHERE 子句
WHERE 子句用于添加过滤条件,相当于 SQL 中的 WHERE:
-- 比较运算
MATCH (p:Person)
WHERE p.age > 25 AND p.age < 40
RETURN p.name, p.age
-- 字符串匹配
MATCH (p:Person)
WHERE p.name STARTS WITH '张' -- 前缀匹配
RETURN p.name
MATCH (p:Person)
WHERE p.name CONTAINS '三' -- 包含匹配
RETURN p.name
-- 正则表达式
MATCH (p:Person)
WHERE p.name =~ '张.*' -- 正则匹配
RETURN p.name
-- IN 运算符
MATCH (p:Person)
WHERE p.name IN ['张三', '李四', '王五']
RETURN p.name
-- 检查属性是否存在
MATCH (p:Person)
WHERE p.email IS NOT NULL
RETURN p.name, p.email
-- 检查标签
MATCH (p)
WHERE p:Person
RETURN p
-- 检查关系是否存在
MATCH (a:Person {name: '张三'}), (b:Person {name: '李四'})
WHERE EXISTS((a)-[:FRIEND]-(b))
RETURN '是朋友' AS 关系
WHERE vs 属性匹配:以下两种写法等效,但属性匹配更简洁:
-- 方式一:属性匹配
MATCH (p:Person {name: '张三', age: 30})
RETURN p
-- 方式二:WHERE 子句
MATCH (p:Person)
WHERE p.name = '张三' AND p.age = 30
RETURN p
更新数据(SET)
SET 语句用于修改节点或关系的属性,或者添加标签。
更新属性
-- 更新单个属性
MATCH (p:Person {name: '张三'})
SET p.age = 31
RETURN p
-- 更新多个属性
MATCH (p:Person {name: '张三'})
SET p.age = 31, p.city = '上海', p.email = '[email protected]'
RETURN p
-- 使用 += 运算符更新部分属性
MATCH (p:Person {name: '张三'})
SET p += {age: 31, city: '上海'}
RETURN p
SET 的覆盖行为:使用 SET p = {key: value} 会完全替换节点的所有属性。使用 SET p += {key: value} 只更新指定的属性。
添加和删除标签
-- 添加标签
MATCH (p:Person {name: '张三'})
SET p:Employee:Manager
RETURN p
-- 删除标签(使用 REMOVE)
MATCH (p:Person {name: '张三'})
REMOVE p:Manager
RETURN p
删除属性
-- 使用 REMOVE 删除属性
MATCH (p:Person {name: '张三'})
REMOVE p.email
RETURN p
-- 使用 SET 设置为 NULL
MATCH (p:Person {name: '张三'})
SET p.email = NULL
RETURN p
删除数据(DELETE 和 DETACH DELETE)
删除操作需要特别注意:删除节点前必须先删除其相关的所有关系。
删除关系
-- 删除特定类型的关系
MATCH ()-[r:FRIEND]->()
WHERE r.since < date('2020-01-01')
DELETE r
-- 删除两个节点之间的关系
MATCH (a:Person {name: '张三'})-[r:FRIEND]-(b:Person {name: '李四'})
DELETE r
删除节点
-- 先删除关系,再删除节点
MATCH (p:Person {name: '张三'})-[r]-()
DELETE r
WITH p
DELETE p
-- 使用 DETACH DELETE 级联删除(推荐)
MATCH (p:Person {name: '张三'})
DETACH DELETE p
DETACH DELETE 的作用:自动删除节点的所有关系,然后删除节点本身。这是最安全的删除方式。
删除所有数据
-- 删除所有节点和关系(危险操作!)
MATCH (n)
DETACH DELETE n
警告:这个操作会清空整个数据库,且不可恢复!
WITH 子句
WITH 子句用于将查询分割成多个部分,将上一个部分的结果传递给下一个部分。这在复杂查询中非常重要。
基本用法
-- 使用 WITH 传递变量
MATCH (p:Person)
WHERE p.age > 25
WITH p
ORDER BY p.age DESC
LIMIT 5
RETURN p.name, p.age
-- 重命名变量
MATCH (p:Person)
WITH p AS person
RETURN person.name
-- 计算中间结果
MATCH (p:Person)-[:FRIEND]->(friend)
WITH p, count(friend) AS friendCount
WHERE friendCount > 5
RETURN p.name, friendCount
WITH 的关键作用:
- 分割复杂查询,使逻辑更清晰
- 在查询中间进行过滤、排序、分页
- 计算聚合值后再进行过滤
WITH 与聚合
-- 先统计每个城市的人数,再筛选
MATCH (p:Person)
WITH p.city AS city, count(p) AS population
WHERE population > 10
RETURN city, population
ORDER BY population DESC
结果处理
排序(ORDER BY)
-- 升序排序(默认)
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age
-- 降序排序
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC
-- 多字段排序
MATCH (p:Person)
RETURN p.name, p.city, p.age
ORDER BY p.city ASC, p.age DESC
分页(SKIP 和 LIMIT)
-- 限制返回数量
MATCH (p:Person)
RETURN p.name
LIMIT 10
-- 跳过前 N 条
MATCH (p:Person)
RETURN p.name
SKIP 10
LIMIT 10
-- 分页查询示例(第 3 页,每页 10 条)
MATCH (p:Person)
RETURN p.name
ORDER BY p.name
SKIP 20 -- (页码 - 1) * 每页条数
LIMIT 10
去重(DISTINCT)
-- 去除重复结果
MATCH (p:Person)-[:LIVES_IN]->(c:City)
RETURN DISTINCT c.name AS 城市
-- 与聚合函数配合
MATCH (p:Person)
RETURN count(DISTINCT p.city) AS 城市数量
综合示例
让我们通过一个完整的社交网络场景来综合运用所学知识:
创建示例数据
// 创建用户节点
CREATE (zhangsan:Person {name: '张三', age: 30, city: '北京'})
CREATE (lisi:Person {name: '李四', age: 28, city: '上海'})
CREATE (wangwu:Person {name: '王五', age: 32, city: '北京'})
CREATE (zhaoliu:Person {name: '赵六', age: 25, city: '广州'})
CREATE (sunqi:Person {name: '孙七', age: 27, city: '上海'})
// 创建朋友关系
CREATE (zhangsan)-[:FRIEND {since: date('2020-01-15')}]->(lisi)
CREATE (lisi)-[:FRIEND {since: date('2019-06-20')}]->(wangwu)
CREATE (wangwu)-[:FRIEND {since: date('2021-03-10')}]->(zhaoliu)
CREATE (sunqi)-[:FRIEND {since: date('2020-08-22')}]->(lisi)
CREATE (zhangsan)-[:COLLEAGUE {since: date('2022-01-01')}]->(wangwu)
实用查询示例
-- 查询张三的所有朋友
MATCH (zhangsan:Person {name: '张三'})-[:FRIEND]-(friend)
RETURN friend.name AS 朋友名称, friend.city AS 所在城市
-- 查询张三的朋友的朋友(潜在好友推荐)
MATCH (zhangsan:Person {name: '张三'})-[:FRIEND]->()-[:FRIEND]->(potential)
WHERE NOT (zhangsan)-[:FRIEND]-(potential)
AND zhangsan <> potential
RETURN DISTINCT potential.name AS 推荐好友, potential.city AS 城市
-- 统计每个城市的人数
MATCH (p:Person)
RETURN p.city AS 城市, count(p) AS 人数
ORDER BY 人数 DESC
-- 查询最年轻的朋友
MATCH (zhangsan:Person {name: '张三'})-[:FRIEND]-(friend)
RETURN friend.name AS 姓名, friend.age AS 年龄
ORDER BY friend.age ASC
LIMIT 1
-- 查询在2020年建立的朋友关系
MATCH ()-[r:FRIEND]->()
WHERE r.since >= date('2020-01-01') AND r.since < date('2021-01-01')
RETURN r.since AS 建立日期
-- 查询共同好友
MATCH (a:Person {name: '张三'})-[:FRIEND]-(common)-[:FRIEND]-(b:Person {name: '孙七'})
WHERE a <> b
RETURN common.name AS 共同好友
最佳实践
1. 使用参数化查询
参数化查询不仅安全,还能提高性能(查询计划缓存):
-- 好的做法:使用参数($name 是参数占位符)
MATCH (p:Person {name: $name})
RETURN p
-- 避免:字符串拼接(有注入风险,且无法利用查询缓存)
-- 错误示例:不要这样做
-- "MATCH (p:Person {name: '" + userInput + "'}) RETURN p"
2. 创建索引加速查询
为常用查询属性创建索引:
-- 创建索引
CREATE INDEX person_name FOR (p:Person) ON (p.name)
-- 利用索引的查询
MATCH (p:Person {name: '张三'})
RETURN p
3. 限制查询结果
始终使用 LIMIT 限制结果数量,避免返回过多数据:
MATCH (p:Person)
RETURN p
LIMIT 100 -- 总是设置合理的上限
4. 使用 EXPLAIN 和 PROFILE 分析查询
-- EXPLAIN:查看执行计划(不执行查询)
EXPLAIN MATCH (p:Person {name: '张三'})-[:FRIEND*5]->()
RETURN p
-- PROFILE:执行查询并显示执行统计
PROFILE MATCH (p:Person {name: '张三'})-[:FRIEND*5]->()
RETURN p
5. 避免笛卡尔积
当 MATCH 子句中有多个独立模式时,会产生笛卡尔积:
-- 避免这样写(可能产生笛卡尔积)
MATCH (a:Person), (b:Person)
WHERE a.city = b.city
RETURN a, b
-- 更好的写法:使用单一模式
MATCH (a:Person)-[:LIVES_IN]->(city)<-[:LIVES_IN]-(b:Person)
WHERE a <> b
RETURN a, b
下一步
掌握了 Cypher 基础操作后,建议继续学习: