跳到主要内容

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 的关键作用

  1. 分割复杂查询,使逻辑更清晰
  2. 在查询中间进行过滤、排序、分页
  3. 计算聚合值后再进行过滤

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 基础操作后,建议继续学习: