从零掌握Neo4j图数据库:告别SQL连表地狱,高效处理复杂关系数据
1. 从“连表地狱”到“关系网络”:为什么我们需要图数据库
如果你写过稍微复杂一点的业务系统,肯定对SQL里的多表连接(JOIN)又爱又恨。爱的是它逻辑清晰,能把分散的数据关联起来;恨的是,当业务关系变得复杂,三张表、五张表甚至十几张表连在一起时,写出来的SQL语句就像一团乱麻,性能也常常惨不忍睹。这还不是最糟的,当需求变成“帮我找出我朋友的朋友的朋友里,谁最近买了某款产品”时,用传统关系型数据库来实现,简直就是一场灾难。你需要不断地递归查询,每一次递归都可能带来一次或多次的JOIN,数据量稍大,查询时间就会呈指数级增长。
这背后的根本原因在于,关系型数据库是为存储规整的“表格”数据而生的,它的核心是“行”和“列”。当数据之间的关系变得和业务本身一样复杂、多变、深度嵌套时,用表格来“模拟”关系就显得力不从心了。关系成了需要额外计算和拼接的“副产品”,而不是数据本身固有的属性。
而图数据库,正是为了解决“关系”的查询与计算而设计的。它的核心思想非常直观:将数据存储为“节点”和“边”。节点代表实体(如人、产品、地点),边代表实体之间的关系(如“购买”、“关注”、“位于”)。这种存储方式让关系成为了一等公民。查询“朋友的朋友”不再是复杂的递归JOIN,而是沿着“朋友”这条边,从起点走两步那么简单。这种操作在图论中称为“遍历”,其效率与关系的深度关系不大,主要取决于起点直接连接的数量,因此性能非常稳定。
Neo4j正是图数据库领域的领头羊和开拓者。它使用原生的图存储引擎,这意味着数据在磁盘上就是以图的结构存放的,而不是用表模拟出来的。它还独创了Cypher查询语言,这是一种声明式的、专门为图查询设计的语言,其语法读起来就像在描述一幅图,非常直观。今天,我们就从零开始,彻底搞懂Neo4j,用它来搭建一个属于你自己的“关系网”,让你亲身感受一下,处理复杂关系数据可以变得多么优雅和高效。
2. Neo4j核心概念全景解析:节点、关系与属性
要玩转Neo4j,首先得理解它的几个核心基石。这些概念构成了图数据模型的全部,比SQL的表、行、列要更贴近我们对真实世界的抽象。
2.1 节点:你的数据宇宙中的实体
节点是图的基本单位,代表我们感兴趣的任何“事物”。在社交网络中,一个人、一家公司、一个地点都可以是一个节点。在推荐系统中,一个用户、一部电影、一件商品也都是节点。
每个节点可以有:
- 标签:相当于节点的分类或类型。一个节点可以有多个标签。例如,一个人节点可以有
Person和Customer两个标签。标签主要用于快速定位某一类节点,是Cypher查询中最重要的过滤条件之一。 - 属性:以键值对的形式存储节点的具体信息。这就像对象的属性或表格中的列。例如,一个
Person节点可以有name: ‘张三’,age: 30,city: ‘北京’等属性。属性支持多种数据类型:数字、字符串、布尔值、列表等。
一个关键的理解:在Neo4j中,没有“表结构”的强制约束。你不需要先定义“Person表必须有哪几列”。你可以随时给任何节点添加任何属性。这种灵活性非常适合业务快速变化的初期,但同时也要求开发者自己对数据模型有清晰的规划,避免后期混乱。
2.2 关系:连接实体的有向纽带
关系是图数据库的灵魂,它连接两个节点,并且总是有方向的。一条关系从一个“起始节点”指向一个“终止节点”。虽然你可以忽略方向进行查询,但定义方向对于理解业务语义至关重要。
每条关系必须有:
- 类型:描述这是一种什么关系。例如,
FOLLOWS、PURCHASED、LIVES_IN。关系类型是查询时遍历路径的主要依据。 - 属性:和节点一样,关系也可以拥有属性。这赋予了关系更强的表达能力。例如,一个
PURCHASED关系可以有amount: 199.99,date: ‘2023-10-01’属性,表示购买金额和日期。这使得关系不仅能表达“是否关联”,还能表达“如何关联”的细节。
关系的独特优势:在关系型数据库中,如果要表示“张三在A公司担任B职位”,你可能需要创建一个“任职记录表”,里面包含人员ID、公司ID、职位等字段。在图数据库中,这直接就是一个Person节点,通过一条类型为WORKS_AT的关系连接到Company节点,而title(职位)可以作为这条关系的属性。这种表达更直观,查询“谁在哪个公司担任什么职位”也更快。
2.3 属性图模型:一切皆可描述
Neo4j采用的属性图模型,就是“节点 + 带属性的关系”的组合。这个模型强大之处在于,它用极其简单的两个概念(节点、关系)和两个扩展(标签、属性),就能描述现实世界中绝大多数复杂的关联场景。
我们可以和关系型数据库做个对比:
- 关系型数据库:世界由“表格”构成,关系通过外键和连接表来模拟。查询关系需要“计算”。
- 属性图数据库(Neo4j):世界由“连接好的实体”构成,关系是存储的一部分。查询关系就是“查找”已有的连接。
这种根本性的差异,决定了它们在处理关联查询时的性能天差地别。当你的查询问题中包含了“多度关系”、“路径寻找”、“模式匹配”这些关键词时,图数据库的优势就会无限放大。
3. Cypher查询语言入门:像描述图画一样查询数据
Cypher是Neo4j的查询语言,它的设计目标就是让查询看起来像它所描述的图一样直观。如果你熟悉SQL,可能会觉得Cypher有些不同,但一旦掌握其模式,你会觉得它非常自然。
3.1 基础语法结构:MATCH, WHERE, RETURN
一个最简单的Cypher查询通常包含三个核心子句,类比SQL的SELECT, FROM, WHERE:
MATCH (p:Person) WHERE p.age > 25 RETURN p.name, p.cityMATCH:这是Cypher的灵魂。它用于描述你要在图中查找的模式。(p:Person)描述了一个模式:找到一个带有Person标签的节点,并给它起个别名p,以便后面引用。更复杂的模式可以是(a:Person)-[:FOLLOWS]->(b:Person),描述“一个人关注另一个人”的模式。WHERE:对MATCH到的模式进行过滤。和SQL的WHERE类似,用于添加条件约束。RETURN:指定查询要返回什么。可以返回节点、关系、它们的属性,或者聚合计算的结果。
实操心得:刚开始写Cypher时,一定要先在纸上或脑海里画出你想找的“图模式”,然后用MATCH子句把这个模式描述出来。这比直接想SQL语句要直观得多。
3.2 创建数据:CREATE 与 MERGE
向图中插入数据主要使用CREATE和MERGE。
CREATE:直接创建新的节点和关系。如果节点已存在,它会创建重复的节点。
CREATE (p:Person {name: ‘李四‘, age: 28})这条语句会创建一个新的Person节点,无论图中是否已存在名为“李四”的人。
MERGE:“有则返回,无则创建”。这是确保数据唯一性的关键命令。它首先尝试匹配你给出的模式,如果匹配不到,则创建它。
MERGE (p:Person {name: ‘王五‘}) ON CREATE SET p.created_at = timestamp() ON MATCH SET p.last_seen = timestamp()这条语句会查找名为“王五”的Person节点,如果找不到就创建,并设置创建时间戳;如果找到了,就更新最后出现的时间戳。这非常适合“upsert”(更新或插入)操作。
注意:
MERGE会对你指定的整个模式进行“有则返回,无则创建”。例如MERGE (a:Person)-[:LIKES]->(b:Movie),它会确保这样一条完整的路径(包括两个节点和一条关系)存在。如果a或b节点不存在,它会一并创建。使用时务必明确你的意图是确保节点唯一,还是确保整个路径唯一。
3.3 核心进阶:关系遍历与路径查询
这才是Cypher和Neo4j真正发光的地方。我们来看几个经典场景。
1. 查找直接关系(一度关系):
// 找到所有关注了“张三”的人 MATCH (follower:Person)-[:FOLLOWS]->(zhang:Person {name: ‘张三‘}) RETURN follower.name2. 查找多度关系(朋友的朋友):
// 找到“张三”的二度关注者(即关注了关注张三的人) MATCH (zhang:Person {name: ‘张三‘})<-[:FOLLOWS]-(friend)-[:FOLLOWS]->(fof) RETURN fof.name这个查询描述了一条路径:从“张三”节点开始,沿着入方向的FOLLOWS关系找到他的关注者(friend),再从这些关注者出发,沿着出方向的FOLLOWS关系找到他们的关注者(fof, friend of friend)。
3. 可变长度路径查询: 当你不确定关系的深度时,可以使用可变长度跳数。
// 找到“张三”的1到3度内的所有关注者 MATCH path = (zhang:Person {name: ‘张三‘})<-[:FOLLOWS*1..3]-(distant_follower) RETURN distant_follower.name, length(path) as depth[:FOLLOWS*1..3]表示沿着FOLLOWS关系,遍历1到3步。length(path)函数返回路径的长度(即跳数)。
4. 寻找最短路径: 这是图数据库的杀手级应用。
// 找出“张三”和“李四”之间最短的“关注”路径 MATCH path = shortestPath( (z:Person {name: ‘张三‘})-[:FOLLOWS*]-(l:Person {name: ‘李四‘}) ) RETURN path, length(path)shortestPath函数会使用高效的图遍历算法(如BFS)找到两点间的最短路径。[:FOLLOWS*]表示任意长度的FOLLOWS路径。
避坑技巧:可变长度遍历[*]非常强大,但如果不加限制(如[*]表示无限深度),在大型图上可能导致性能问题甚至内存溢出。务必结合实际业务,给一个合理的上下界(如[*1..5])。
4. 从零搭建实战:构建一个社交-兴趣关系网络
现在,让我们把理论付诸实践,一步步搭建一个模拟的“社交-兴趣关系网”。这个网络包含用户、他们之间的关注关系、他们的兴趣标签,以及他们对某些内容(如文章、视频)的互动。
4.1 环境准备与Neo4j安装
首先,你需要一个运行中的Neo4j实例。对于初学者,最推荐的方式是使用Neo4j Desktop。它是一个集成的开发环境,包含了数据库、浏览器UI和管理工具,特别适合学习和开发。
- 下载与安装:访问Neo4j官网,下载对应你操作系统的Neo4j Desktop版本。安装过程非常简单,一路下一步即可。
- 创建数据库:启动Neo4j Desktop,点击“New”创建一个新的数据库项目。你可以给它起个名字,比如
MySocialGraph。选择最新的稳定版本(如5.x)。 - 启动与连接:创建完成后,点击“Start”启动数据库。状态变为“Running”后,点击“Open”按钮,会直接在浏览器中打开Neo4j Browser,这是你与数据库交互的主要界面。
- 初次登录:默认的用户名和密码都是
neo4j。首次登录会强制要求你修改密码,请务必记住新密码。
至此,一个本地的、单机版的Neo4j图数据库就准备就绪了。Neo4j Browser的交互界面分为上方的输入框(用于输入Cypher命令)和下方的结果展示区。
4.2 数据模型设计与初始化
在动手插入数据前,先设计好我们的数据模型。我们的图将包含以下元素:
- 节点标签:
Person: 用户。属性:userId,name,city。Interest: 兴趣标签。属性:tagName。Content: 内容(如文章/视频)。属性:contentId,title,type。
- 关系类型:
FOLLOWS:Person->Person。属性:since(关注日期)。HAS_INTEREST:Person->Interest。LIKED:Person->Content。属性:timestamp。POSTED:Person->Content。
现在,我们在Neo4j Browser中输入Cypher语句来创建初始数据。
步骤一:清空测试数据(谨慎操作)如果这不是一个新数据库,可以先运行以下命令删除所有现有数据。在生产环境切勿使用。
MATCH (n) DETACH DELETE n;DETACH DELETE会删除节点及其所有连接的关系。
步骤二:创建人物节点
CREATE (alice:Person {userId: ‘U001‘, name: ‘Alice‘, city: ‘New York‘}), (bob:Person {userId: ‘U002‘, name: ‘Bob‘, city: ‘London‘}), (charlie:Person {userId: ‘U003‘, name: ‘Charlie‘, city: ‘San Francisco‘}), (diana:Person {userId: ‘U004‘, name: ‘Diana‘, city: ‘Beijing‘}), (eve:Person {userId: ‘U005‘, name: ‘Eve‘, city: ‘Sydney‘});步骤三:创建关注关系网络
MATCH (a:Person {name: ‘Alice‘}), (b:Person {name: ‘Bob‘}), (c:Person {name: ‘Charlie‘}), (d:Person {name: ‘Diana‘}), (e:Person {name: ‘Eve‘}) CREATE (a)-[:FOLLOWS {since: ‘2023-01-01‘}]->(b), (a)-[:FOLLOWS {since: ‘2023-02-15‘}]->(c), (b)-[:FOLLOWS {since: ‘2022-11-30‘}]->(d), (c)-[:FOLLOWS {since: ‘2023-03-10‘}]->(a), (c)-[:FOLLOWS {since: ‘2023-04-01‘}]->(e), (d)-[:FOLLOWS {since: ‘2023-05-20‘}]->(b), (e)-[:FOLLOWS {since: ‘2023-06-05‘}]->(a);步骤四:创建兴趣标签和关联
CREATE (ai:Interest {tagName: ‘Artificial Intelligence‘}), (music:Interest {tagName: ‘Music‘}), (travel:Interest {tagName: ‘Travel‘}), (sports:Interest {tagName: ‘Sports‘}); MATCH (a:Person {name: ‘Alice‘}), (b:Person {name: ‘Bob‘}), (c:Person {name: ‘Charlie‘}), (ai:Interest {tagName: ‘Artificial Intelligence‘}), (music:Interest {tagName: ‘Music‘}), (travel:Interest {tagName: ‘Travel‘}) CREATE (a)-[:HAS_INTEREST]->(ai), (a)-[:HAS_INTEREST]->(music), (b)-[:HAS_INTEREST]->(travel), (c)-[:HAS_INTEREST]->(ai);步骤五:创建内容和互动
CREATE (article1:Content {contentId: ‘C001‘, title: ‘The Future of AI‘, type: ‘Article‘}), (video1:Content {contentId: ‘C002‘, title: ‘Guitar Basics Tutorial‘, type: ‘Video‘}); MATCH (a:Person {name: ‘Alice‘}), (c:Person {name: ‘Charlie‘}), (art:Content {contentId: ‘C001‘}), (vid:Content {contentId: ‘C002‘}) CREATE (a)-[:POSTED]->(art), (c)-[:POSTED]->(vid), (a)-[:LIKED {timestamp: ‘2023-10-01T10:00:00‘}]->(vid), (c)-[:LIKED {timestamp: ‘2023-10-02T14:30:00‘}]->(art);执行完以上所有语句后,一个包含5个人物、4个兴趣、2个内容以及若干关注、兴趣、互动关系的微型社交图就构建完成了。你可以在Neo4j Browser中运行MATCH (n) RETURN n来可视化整个图谱,直观地看到所有节点和连接。
4.3 执行复杂关系查询:体验图遍历的威力
现在,让我们提出几个在关系型数据库中会很棘手,但在Neo4j中却非常简单的查询。
查询1:找出Alice可能认识的人(二度人脉)
MATCH (alice:Person {name: ‘Alice‘})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(suggestion) WHERE alice <> suggestion AND NOT (alice)-[:FOLLOWS]->(suggestion) RETURN DISTINCT suggestion.name AS potential_connection, count(friend) AS mutual_friends ORDER BY mutual_friends DESC;解析:
MATCH找到从Alice出发,经过两段FOLLOWS关系的路径。WHERE子句确保推荐的不是Alice自己,并且Alice目前还没有直接关注这个人。RETURN返回推荐人的名字,并计算有多少个共同朋友(friend)作为权重。ORDER BY按共同朋友数降序排列,共同朋友越多,推荐优先级越高。
这个查询清晰地展示了“朋友的朋友”逻辑,并且轻松地计算了共同好友数作为推荐强度,如果用SQL实现,需要多次自连接和子查询,复杂且低效。
查询2:寻找对AI感兴趣,且喜欢Charlie发布的视频的人
MATCH (ai:Interest {tagName: ‘Artificial Intelligence‘})<-[:HAS_INTEREST]-(person:Person) MATCH (person)-[:LIKED]->(content)<-[:POSTED]-(charlie:Person {name: ‘Charlie‘}) RETURN person.name, content.title;解析:这个查询融合了两种关系。首先找到对AI感兴趣的人,然后在这些人的基础上,进一步筛选出喜欢了由Charlie发布的内容的人。两个MATCH子句相当于在逐步缩小结果集,逻辑非常清晰。
查询3:发现兴趣社群(通过共同兴趣连接的用户群)
MATCH (p1:Person)-[:HAS_INTEREST]->(interest)<-[:HAS_INTEREST]-(p2:Person) WHERE p1.userId < p2.userId // 避免重复配对 (A,B) 和 (B,A) RETURN p1.name AS person1, p2.name AS person2, interest.tagName AS common_interest ORDER BY common_interest;这个查询找出了所有拥有共同兴趣的用户对。它揭示了基于兴趣的潜在社交连接,对于社区发现或精准营销非常有价值。
5. 性能优化与生产实践要点
当你从学习测试转向真实生产环境时,以下几个方面的考虑至关重要。
5.1 索引与约束:加速查询的基石
和关系型数据库一样,没有索引的图数据库在查询时也会进行全图扫描,性能极差。Neo4j主要使用两种索引:
单属性索引:针对节点的某个标签的某个属性创建。
CREATE INDEX person_name_index FOR (p:Person) ON (p.name);这会在
Person节点的name属性上创建索引,加速WHERE p.name = ‘Alice‘这类查询。复合索引:针对节点的某个标签的多个属性创建。
CREATE INDEX person_city_age_index FOR (p:Person) ON (p.city, p.age);这个索引对同时涉及
city和age的查询最有效。唯一性约束:它同时也会自动创建一个索引。
CREATE CONSTRAINT unique_person_userId FOR (p:Person) REQUIRE p.userId IS UNIQUE;这确保了
Person节点的userId属性全局唯一,并且查询该属性会非常快。
重要提示:索引不是免费的,它会增加数据写入和存储的开销。只为最常用的查询条件创建索引。通常,用于
MATCH模式起点(如(p:Person {name: ...}))的属性是创建索引的首选。
5.2 查询优化心法
- 从最小结果集开始:在编写
MATCH模式时,尽量让模式的开头部分能匹配到尽可能少的节点。例如,MATCH (p:Person {name: ‘Alice‘})-[:FOLLOWS]->(f)比MATCH (p:Person)-[:FOLLOWS]->(f)要好得多,因为前者一开始就通过属性过滤锁定了单个节点。 - 善用
PROFILE和EXPLAIN:在查询语句前加上PROFILE,Neo4j会执行查询并返回详细的执行计划,包括每个操作符消耗的数据库命中次数(DB Hits)。这是分析查询性能瓶颈的最直接工具。EXPLAIN则只显示计划而不执行。
查看结果中的“DB Hits”,数值越大的步骤通常是瓶颈所在。PROFILE MATCH (p:Person)-[:FOLLOWS]->(f) RETURN p.name, count(f); - 避免笛卡尔积:当多个
MATCH子句没有通过关系或属性关联时,它们会产生所有可能的组合(笛卡尔积),导致中间结果集爆炸。务必确保你的MATCH模式是连接在一起的。 - 限制路径遍历深度和结果集大小:对于可变长度遍历
[*],务必设置上限。使用LIMIT子句限制返回的结果数量,尤其是在探索性查询中。
5.3 与应用程序集成
在实际项目中,你肯定不会一直用浏览器操作。Neo4j提供了丰富的官方驱动来支持各种编程语言。
以Python为例,使用官方neo4j驱动:
安装驱动:
pip install neo4j连接与查询:
from neo4j import GraphDatabase URI = “bolt://localhost:7687“ # Neo4j默认的Bolt协议端口 AUTH = (“neo4j“, “your_password_here“) # 替换为你的密码 driver = GraphDatabase.driver(URI, auth=AUTH) def get_friends_of_friends(tx, name): query = “““ MATCH (p:Person {name: $name})-[:FOLLOWS]->(friend)-[:FOLLOWS]->(fof) RETURN fof.name AS name “““ result = tx.run(query, name=name) return [record[“name“] for record in result] with driver.session() as session: friends_of_friends = session.execute_read(get_friends_of_friends, “Alice“) print(friends_of_friends) driver.close()关键点:
- 使用参数化查询(
$name),绝对不要用字符串拼接,这是防止注入攻击的基本要求。 session.execute_read用于执行读查询,session.execute_write用于执行写查询。- 驱动会自动管理连接池,确保高效地复用连接。
- 使用参数化查询(
6. 常见问题与故障排查实录
在实际使用中,你肯定会遇到各种问题。这里记录了一些典型场景和解决方法。
6.1 查询性能突然变慢
- 可能原因1:数据量增长,缺少索引。
- 排查:对慢查询使用
PROFILE命令,查看初始的“AllNodesScan”或“Filter”操作是否消耗了大量DB Hits。 - 解决:为
MATCH模式起点的节点属性创建索引。
- 排查:对慢查询使用
- 可能原因2:查询语句编写不当,产生了巨大的中间结果。
- 排查:检查
PROFILE输出,看是否有步骤产生了远超预期的记录数。检查是否无意中造成了笛卡尔积。 - 解决:重写查询,确保模式有效关联,尽早过滤,使用
LIMIT。
- 排查:检查
- 可能原因3:JVM堆内存或页面缓存不足。
- 排查:检查Neo4j Desktop的数据库管理界面或日志,查看内存使用情况。
- 解决:对于生产环境,需要根据数据量大小调整
neo4j.conf中的dbms.memory.heap.initial_size、dbms.memory.heap.max_size和dbms.memory.pagecache.size参数。原则是:页面缓存大小应略大于存储文件总大小,堆内存用于查询执行和事务管理。
6.2 “OutOfMemory”错误
- 可能原因1:单次查询加载了过多数据到内存。
- 解决:优化查询,避免返回全部数据。使用聚合函数(如
count,collect)在数据库端进行汇总,或使用LIMIT和SKIP进行分页。
- 解决:优化查询,避免返回全部数据。使用聚合函数(如
- 可能原因2:可变长度遍历
[*]没有设置上限,在图中有环时可能产生无限路径或路径爆炸。- 解决:务必为可变长度遍历设置合理的上限,如
[:FOLLOWS*1..5]。
- 解决:务必为可变长度遍历设置合理的上限,如
- 可能原因3:JVM堆内存设置过低。
- 解决:适当调高堆内存配置,但不要超过物理内存的70%。
6.3 数据一致性疑问
- 问题:Neo4j是ACID兼容的数据库吗?
- 回答:是的。Neo4j完全支持ACID(原子性、一致性、隔离性、持久性)事务。所有的写操作(CREATE, MERGE, SET, DELETE等)都在事务中执行。你可以显式地使用
BEGIN、COMMIT、ROLLBACK来管理事务,在驱动程序中,一个会话(Session)内执行的一系列操作通常也默认在一个事务中。
6.4 如何批量导入海量数据
使用CREATE语句一条条插入数百万数据是极其低效的。Neo4j提供了高性能的批量导入工具:
neo4j-admin database import:这是最快的全量导入方式,适用于从CSV文件初始化空数据库。它需要数据库处于停止状态,直接构建底层存储文件。LOAD CSVCypher命令:在Neo4j Browser或驱动中执行,可以从本地或远程URL加载CSV文件。它在一个事务中执行,适合导入几十万到百万级的数据。需要配合USING PERIODIC COMMIT来分批提交,避免内存溢出。USING PERIODIC COMMIT 10000 LOAD CSV WITH HEADERS FROM ‘file:///users.csv‘ AS row MERGE (p:Person {userId: row.userId}) SET p.name = row.name, p.city = row.city;- 官方ETL工具(APOC库)或Neo4j ETL Tool:提供更复杂、功能更全的数据管道能力。
个人体会是,对于超大规模初始导入,neo4j-admin import是不二之选;对于持续的数据流或增量更新,则结合使用LOAD CSV或应用程序驱动。在批量操作时,务必关注日志和内存使用,做好错误处理和重试机制。
从被SQL多表连接折磨,到用Neo4j优雅地表达和查询复杂关系,这种转变不仅仅是换了一个工具,更是思维模式的一次升级。当你开始用“图”的视角看待数据中的连接时,很多曾经棘手的问题会豁然开朗。当然,图数据库并非银弹,它擅长处理高度互联、深度关系的查询,但对于大规模批量分析、频繁的全表扫描类操作,可能不如一些列式存储数据库。正确的姿势是根据业务场景,选择合适的工具,甚至组合使用。Neo4j的学习曲线起初可能有点陡峭,但一旦你掌握了Cypher的思维,并亲手构建了几个关系网络后,你会发现,处理关系数据,本就该如此直观和高效。
