




















图数据库的核心优势之一,就是天然支持高效的多跳路径查询。本文将以「社交网络好友关系」为实例,从零开始讲解 Neo4j 中多跳查询的语法、场景、进阶技巧与性能边界。
在图数据模型中,数据以节点(Node)和关系(Relationship)存储。一次「跳(Hop)」指从一个节点出发,沿着一条关系到达相邻节点的过程。多跳查询就是沿着关系链路连续遍历多层节点的操作,典型场景包括:
Neo4j 使用 Cypher 查询语言,通过可变长度路径(Variable-length Path)语法原生支持多跳查询,无需像 SQL 那样写多层 JOIN。
我们构建一个简化的社交网络图谱:
Person(人物)、Hobby(爱好)FRIENDS_WITH(好友关系,双向)、LIKES(喜欢某爱好)执行以下 Cypher 语句,构建包含 8 个人物、3 种爱好的示例图谱:
// 清空数据库(谨慎执行)
MATCH (n) DETACH DELETE n;
// 创建人物节点
CREATE
(alice:Person {name: 'Alice', age: 28, city: '北京'}),
(bob:Person {name: 'Bob', age: 30, city: '北京'}),
(charlie:Person {name: 'Charlie', age: 25, city: '上海'}),
(david:Person {name: 'David', age: 32, city: '深圳'}),
(emma:Person {name: 'Emma', age: 27, city: '北京'}),
(frank:Person {name: 'Frank', age: 35, city: '上海'}),
(grace:Person {name: 'Grace', age: 29, city: '深圳'}),
(henry:Person {name: 'Henry', age: 31, city: '北京'});
// 创建爱好节点
CREATE
(coding:Hobby {name: '编程'}),
(music:Hobby {name: '音乐'}),
(hiking:Hobby {name: '徒步'});
// 创建好友关系(无方向,双向互通)
MATCH (alice:Person {name: 'Alice'})
MATCH (bob:Person {name: 'Bob'})
MATCH (charlie:Person {name: 'Charlie'})
MATCH (david:Person {name: 'David'})
MATCH (emma:Person {name: 'Emma'})
MATCH (frank:Person {name: 'Frank'})
MATCH (grace:Person {name: 'Grace'})
MATCH (henry:Person {name: 'Henry'})
CREATE
(alice)-[:FRIENDS_WITH]->(bob),
(bob)-[:FRIENDS_WITH]->(charlie),
(charlie)-[:FRIENDS_WITH]->(david),
(alice)-[:FRIENDS_WITH]->(emma),
(emma)-[:FRIENDS_WITH]->(frank),
(frank)-[:FRIENDS_WITH]->(grace),
(david)-[:FRIENDS_WITH]->(grace),
(emma)-[:FRIENDS_WITH]->(henry),
(henry)-[:FRIENDS_WITH]->(frank);
// 创建爱好关系
MATCH (alice:Person {name: 'Alice'})
MATCH (bob:Person {name: 'Bob'})
MATCH (charlie:Person {name: 'Charlie'})
MATCH (david:Person {name: 'David'})
MATCH (emma:Person {name: 'Emma'})
MATCH (frank:Person {name: 'Frank'})
MATCH (grace:Person {name: 'Grace'})
MATCH (henry:Person {name: 'Henry'})
MATCH (coding:Hobby {name: '编程'})
MATCH (music:Hobby {name: '音乐'})
MATCH (hiking:Hobby {name: '徒步'})
CREATE
(alice)-[:LIKES]->(coding),
(bob)-[:LIKES]->(coding),
(charlie)-[:LIKES]->(music),
(david)-[:LIKES]->(hiking),
(emma)-[:LIKES]->(music),
(frank)-[:LIKES]->(coding),
(grace)-[:LIKES]->(hiking),
(henry)-[:LIKES]->(music);
执行完成后,你会得到如下拓扑结构:
Alice — Bob — Charlie — David
| |
Emma — Frank ———— Grace
\ /
Henry
从 Alice 出发,查找她的直接好友(1 跳):
MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH]->(friend)
RETURN friend.name AS 直接好友, friend.city AS 城市;
结果:
| 直接好友 | 城市 |
|---|---|
| Bob | 北京 |
| Emma | 北京 |
这是最基础的 1 跳遍历,对应 SQL 中的一次 JOIN。
从 Alice 出发,查找「朋友的朋友」(2 跳),排除 Alice 自己和直接好友:
MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH*2]->(fof)
WHERE fof.name <> 'Alice'
RETURN fof.name AS 朋友的朋友, fof.city AS 城市;
语法说明: [:FRIENDS_WITH*2] 表示精确匹配长度为 2 的 FRIENDS_WITH 关系路径。
结果:
| 朋友的朋友 | 城市 |
|---|---|
| Charlie | 上海 |
| Frank | 上海 |
| Henry | 北京 |
可以看到,Alice 通过 Bob 认识了 Charlie,通过 Emma 认识了 Frank 和 Henry。
继续扩展到 3 跳,查找三度人脉:
MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH*3]->(fofof)
RETURN DISTINCT fofof.name AS 三度人脉, fofof.city AS 城市
ORDER BY fofof.name;
结果:
| 三度人脉 | 城市 |
|---|---|
| David | 深圳 |
| Grace | 深圳 |
| Frank | 上海 |
这就是典型的「六度分隔理论」在图数据库中的直观实现。
实际业务中,我们往往不局限于固定跳数,而是希望查询「1 到 N 跳范围内的所有节点」。
使用 *min..max 语法指定跳数范围:
// 查询 Alice 1~3 跳范围内的所有人
MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH*1..3]->(contact)
RETURN DISTINCT contact.name AS 人脉, contact.city AS 城市
ORDER BY contact.name;
结果包含 1 跳、2 跳、3 跳的全部 7 个人(除 Alice 自己外的全部节点)。
*..3:最多 3 跳(0~3 跳,包含起点)*2..:最少 2 跳,不设上限(不推荐,可能导致全图遍历)// 0~2 跳,包含 Alice 本人
MATCH path = (:Person {name: 'Alice'})-[:FRIENDS_WITH*..2]->(n)
RETURN nodes(path) AS 路径节点;
⚠️ 重要提醒:不指定上界的
*n..是高危写法,在大图中会引发性能灾难,生产环境必须设置最大深度。
纯路径遍历只是基础,真实场景往往需要叠加过滤条件、关系属性、多类型关系等。
查找 Alice 的 2 跳好友中,住在北京的人:
MATCH (p:Person {name: 'Alice'})-[:FRIENDS_WITH*2]->(fof)
WHERE fof.city = '北京' AND fof <> p
RETURN fof.name AS 北京二度好友, fof.age AS 年龄;
结果: Henry(北京)
从「人→好友→爱好」跨越两种关系类型,做兴趣推荐:查找 Alice 的好友们都喜欢什么爱好,用于给 Alice 做推荐。
MATCH (:Person {name: 'Alice'})-[:FRIENDS_WITH]->(friend)-[:LIKES]->(hobby)
RETURN hobby.name AS 好友喜欢的爱好, collect(friend.name) AS 喜欢的好友列表;
结果:
| 好友喜欢的爱好 | 喜欢的好友列表 |
|---|---|
| 编程 | [Bob] |
| 音乐 | [Emma] |
进一步扩展到 2 跳好友的爱好,实现更广泛的兴趣发现:
MATCH (:Person {name: 'Alice'})-[:FRIENDS_WITH*2]->(fof)-[:LIKES]->(hobby)
RETURN hobby.name AS 爱好, count(DISTINCT fof) AS 喜欢人数
ORDER BY 喜欢人数 DESC;
查找两个节点之间的最短路径(跳数最少),这是多跳查询的经典变体:
MATCH path = shortestPath(
(:Person {name: 'Alice'})-[:FRIENDS_WITH*]-(:Person {name: 'Grace'})
)
RETURN
length(path) AS 最短跳数,
[n IN nodes(path) | n.name] AS 路径;
结果:
["Alice", "Emma", "Frank", "Grace"]注意这里关系使用了无方向写法 -[:FRIENDS_WITH]-(不带箭头),因为好友关系是双向互通的。
多跳查询的遍历量随深度呈指数级增长。假设图中每个节点平均有 d 个邻居(此处 d=10):
| 跳数 | 理论最大遍历节点数 |
|---|---|
| 1 跳 | 10 |
| 2 跳 | 100 |
| 3 跳 | 1,000 |
| 4 跳 | 10,000 |
| 5 跳 | 100,000 |
| 6 跳 | 1,000,000 |
注:以上是最坏情况的理论上限,实际遍历量通常远低于此值。因为社交网络中好友圈高度重叠(Alice 的朋友很可能也互相认识),大量路径会汇聚到已访问过的节点,实际扫描的节点数远小于理论值。
因此实际业务中,绝大多数场景限制在 3~4 跳以内是合理的。超过 5 跳的查询在稠密图中极易造成内存或时间超限。
*min..max 明确上界,避免 *.. 无界遍历name、id)建立索引或唯一约束CREATE INDEX FOR (p:Person) ON (p.name);
DISTINCT 避免结果集膨胀和重复的网络传输MATCH (n) 开头的全图扫描*..n 包含 0 跳(即起点自身),如果业务上需要排除起点,记得加 WHERE n <> start 或指定最小跳数 *1..n-[:REL]- 会同时遍历入边和出边,工作量是定向遍历的 2 倍,非必要时不要滥用无方向写法Neo4j 的多跳查询是图数据库区别于关系型数据库的核心能力之一。通过本文的社交网络实例,我们掌握了:
[:REL*n] 精确遍历 N 层[:REL*min..max] 灵活指定深度在真实项目中,多跳查询广泛应用于社交推荐、金融风控、供应链分析、知识图谱推理等场景。掌握好路径遍历的语法与性能控制,就能充分发挥图数据库的价值。
如果你想进一步深入,可以继续探索:带权重的最短路径(dijkstra 算法)、全路径查找 allShortestPaths、以及基于 apoc.path.expand 的更高级路径扩展过程。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。