Neo4j 图数据库入门(三):从会写到写好——MERGE 防重、聚合分析、索引约束与完整实战

2624 字
13 分钟
Neo4j 图数据库入门(三):从会写到写好——MERGE 防重、聚合分析、索引约束与完整实战

本系列导航

场景依旧是「电影社交网络」。第二篇结尾我们把数据库清空了——别慌,本篇第 1 节学 MERGE 时会顺手把全图重建回来,第 5 节再给一段”生产级标准姿势”的完整实战脚本收官。


1. MERGE:写数据的正确姿势#

1.1 CREATE 的重复问题#

在第二篇埋过一个伏笔:CREATE 每次执行都会新建。跑两遍这句:

CREATE (p:Person {name: '张三', age: 30})
RETURN p;

图里就有两个张三。批量导入数据时,脚本重跑一次、脏数据就翻一倍。

1.2 MERGE:存在则匹配,不存在才创建#

MERGE = “有就复用,没有才建”。跑多少遍都只有一个张三(这种”跑多少遍结果都一样”的特性叫幂等):

MERGE (p:Person {name: '张三'})
RETURN p;

1.3 ON CREATE / ON MATCH:区分”新建”与”命中”#

一个关键细节:MERGE 括号里的属性全部参与匹配。所以最佳实践是——括号里只放”身份键”,其余属性用 ON CREATE SET 补

MERGE (p:Person {name: '李四'})
ON CREATE SET p.age = 28, p.gender = '男', p.createdAt = datetime()
ON MATCH SET p.lastSeen = datetime()
RETURN p;
  • 第一次执行:节点不存在 → 创建,触发 ON CREATE(补全属性 + 记录创建时间)
  • 之后再执行:节点已存在 → 命中,触发 ON MATCH(更新最后出现时间)

这个模式天然适合做”数据同步”类任务。

1.4 关系防重#

MERGE 同样适用于关系,确保同一对节点间的同类关系只有一条:

MATCH (a:Person {name: '张三'}), (b:Person {name: '李四'})
MERGE (a)-[r:KNOWS]->(b)
ON CREATE SET r.since = 2018
ON MATCH SET r.lastInteract = datetime()
RETURN r;

1.5 顺手重建电影社交网络#

把上面学的 MERGE 用起来,一键恢复全系列的数据。直接整段粘贴执行(重跑也无害,这就是 MERGE 的意义):

// ===== 节点:括号里只放身份键,属性交给 ON CREATE SET =====
MERGE (p1:Person {name: '张三'}) ON CREATE SET p1.age = 30, p1.gender = '男';
MERGE (p2:Person {name: '李四'}) ON CREATE SET p2.age = 28, p2.gender = '男';
MERGE (p3:Person {name: '王五'}) ON CREATE SET p3.age = 35, p3.gender = '男';
MERGE (p4:Person {name: '赵六'}) ON CREATE SET p4.age = 26, p4.gender = '女';
MERGE (m1:Movie {title: '流浪地球'}) ON CREATE SET m1.year = 2019, m1.rating = 7.9;
MERGE (m2:Movie {title: '三体'}) ON CREATE SET m2.year = 2023, m2.rating = 8.5;
MERGE (m3:Movie {title: '满江红'}) ON CREATE SET m3.year = 2023, m3.rating = 7.0;
MERGE (c1:Company {name: '中影股份'}) ON CREATE SET c1.founded = 1999;
MERGE (c2:Company {name: '光线传媒'}) ON CREATE SET c2.founded = 1998;
// ===== 关系:先 MATCH 两端,再 MERGE 关系 =====
MATCH (a:Person {name: '张三'}), (b:Person {name: '李四'})
MERGE (a)-[:KNOWS {since: 2018}]->(b);
MATCH (a:Person {name: '李四'}), (b:Person {name: '王五'})
MERGE (a)-[:KNOWS {since: 2020}]->(b);
MATCH (a:Person {name: '王五'}), (b:Person {name: '赵六'})
MERGE (a)-[:KNOWS {since: 2021}]->(b);
MATCH (a:Person {name: '张三'}), (b:Person {name: '王五'})
MERGE (a)-[:KNOWS {since: 2019}]->(b);
MATCH (p:Person {name: '张三'}), (m:Movie {title: '流浪地球'})
MERGE (p)-[:ACTED_IN {role: '工程师'}]->(m);
MATCH (p:Person {name: '李四'}), (m:Movie {title: '流浪地球'})
MERGE (p)-[:DIRECTED]->(m);
MATCH (p:Person {name: '赵六'}), (m:Movie {title: '三体'})
MERGE (p)-[:ACTED_IN {role: '科学家'}]->(m);
MATCH (p:Person {name: '王五'}), (c:Company {name: '中影股份'})
MERGE (p)-[:WORKS_AT {position: '制片人'}]->(c);
MATCH (p:Person {name: '张三'}), (m:Movie {title: '满江红'})
MERGE (p)-[:REVIEWED {score: 8}]->(m);

跑完后图已恢复。练手任务:还记得第二篇 1.4 创建的《孤注一掷》吗?用一条 MERGE 把它和赵六的 ACTED_IN 关系补回来,检验本节所学。


2. 聚合与排序#

2.1 聚合函数#

和 SQL 几乎同名同义:count 计数、collect 收集成列表、avg / max / min / sum 统计:

// 库里一共多少人
MATCH (p:Person)
RETURN count(p) AS totalPersons;
// 张三认识几个人
MATCH (:Person {name: '张三'})-[:KNOWS]->(friend:Person)
RETURN count(friend) AS friendCount;
// 每部电影有多少人参演(按电影分组)
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
RETURN m.title, count(p) AS actorCount;
// 全部电影的评分概况
MATCH (m:Movie)
RETURN
avg(m.rating) AS avgRating,
max(m.rating) AS maxRating,
min(m.rating) AS minRating;
// 把张三的朋友收集成一个列表(collect:分组聚合的利器)
MATCH (:Person {name: '张三'})-[:KNOWS]->(friend:Person)
RETURN collect(friend.name) AS friendList;

不用写 GROUP BY——Cypher 根据 RETURN 中”非聚合的键”自动分组。最后一条的 collect 值得单独记住:把一列值收进一个数组,是图数据做概览展示的常用手法。

2.2 排序与分页#

// 电影按评分降序
MATCH (m:Movie)
RETURN m.title, m.rating
ORDER BY m.rating DESC;
// 分页:按年龄降序,跳过第 1 条,取 2 条
MATCH (p:Person)
RETURN p.name, p.age
ORDER BY p.age DESC
SKIP 1
LIMIT 2;

3. WITH 子句:查询管道的中间站#

WITH 把上一段查询的结果”暂存”下来,交给下一段继续处理——相当于在一条 Cypher 里做子查询,让复杂查询变成流水线:

// 先筛出评分高于 7 的电影,再收集每部电影的演员
MATCH (m:Movie)
WHERE m.rating > 7
WITH m
MATCH (m)<-[:ACTED_IN]-(p:Person)
RETURN m.title, collect(p.name) AS actors;
// 找出"社交达人":先统计每人认识多少人,再过滤
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WITH p, count(friend) AS friendCount
WHERE friendCount > 1
RETURN p.name, friendCount;

第二条是 WITH 最经典的用法:聚合结果不能直接跟 WHERE,必须先用 WITH 落地,再过滤。读数据流:MATCH 找关系 → WITH 算出每人朋友数 → WHERE 筛选 → RETURN 输出。运行结果只有张三(他认识 2 人)。


4. 索引与约束#

4.1 索引:让精确匹配起飞#

数据量大了之后,MATCH (p:Person {name: '张三'}) 这类查找需要索引加速:

// 为 Person.name 建索引
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name);
// 复合索引
CREATE INDEX movie_title_year IF NOT EXISTS
FOR (m:Movie) ON (m.title, m.year);
// 查看已有索引
SHOW INDEXES;

4.2 唯一约束:从数据库层面杜绝重复#

CREATE CONSTRAINT unique_person_name IF NOT EXISTS
FOR (p:Person) REQUIRE p.name IS UNIQUE;
CREATE CONSTRAINT unique_movie_title IF NOT EXISTS
FOR (m:Movie) REQUIRE m.title IS UNIQUE;
SHOW CONSTRAINTS;

约束建立后,谁再想 CREATE 第二个张三,数据库会直接拒绝。

4.3 双保险原则#

先建唯一约束,再用 MERGE 导入数据。

  • 约束:挡住绕过程序逻辑的脏写入
  • MERGE:让导入脚本天然幂等、可安全重跑

这两招合在一起,就是生产环境图数据初始化的标准姿势——正好引出本篇的压轴戏。


5. 完整实战:从零构建电影社交图谱#

把全系列知识串成一条线的”标准初始化脚本”,可直接整段粘贴到 Neo4j Browser 运行,重复执行也安全IF NOT EXISTS + MERGE 保证幂等):

// ============ 第一步:清空数据库(从零开始) ============
MATCH (n) DETACH DELETE n;
// ============ 第二步:约束先行 ============
CREATE CONSTRAINT unique_person_name IF NOT EXISTS
FOR (p:Person) REQUIRE p.name IS UNIQUE;
CREATE CONSTRAINT unique_movie_title IF NOT EXISTS
FOR (m:Movie) REQUIRE m.title IS UNIQUE;
// ============ 第三步:索引加速 ============
CREATE INDEX person_name IF NOT EXISTS
FOR (p:Person) ON (p.name);
// ============ 第四步:MERGE 节点 ============
MERGE (p1:Person {name: '张三'}) ON CREATE SET p1.age = 30, p1.gender = '男';
MERGE (p2:Person {name: '李四'}) ON CREATE SET p2.age = 28, p2.gender = '男';
MERGE (p3:Person {name: '王五'}) ON CREATE SET p3.age = 35, p3.gender = '男';
MERGE (p4:Person {name: '赵六'}) ON CREATE SET p4.age = 26, p4.gender = '女';
MERGE (m1:Movie {title: '流浪地球'}) ON CREATE SET m1.year = 2019, m1.rating = 7.9;
MERGE (m2:Movie {title: '三体'}) ON CREATE SET m2.year = 2023, m2.rating = 8.5;
MERGE (m3:Movie {title: '满江红'}) ON CREATE SET m3.year = 2023, m3.rating = 7.0;
MERGE (c1:Company {name: '中影股份'}) ON CREATE SET c1.founded = 1999;
MERGE (c2:Company {name: '光线传媒'}) ON CREATE SET c2.founded = 1998;
// ============ 第五步:MERGE 关系 ============
MATCH (a:Person {name: '张三'}), (b:Person {name: '李四'})
MERGE (a)-[:KNOWS {since: 2018}]->(b);
MATCH (a:Person {name: '李四'}), (b:Person {name: '王五'})
MERGE (a)-[:KNOWS {since: 2020}]->(b);
MATCH (a:Person {name: '王五'}), (b:Person {name: '赵六'})
MERGE (a)-[:KNOWS {since: 2021}]->(b);
MATCH (a:Person {name: '张三'}), (b:Person {name: '王五'})
MERGE (a)-[:KNOWS {since: 2019}]->(b);
MATCH (p:Person {name: '张三'}), (m:Movie {title: '流浪地球'})
MERGE (p)-[:ACTED_IN {role: '工程师'}]->(m);
MATCH (p:Person {name: '李四'}), (m:Movie {title: '流浪地球'})
MERGE (p)-[:DIRECTED]->(m);
MATCH (p:Person {name: '赵六'}), (m:Movie {title: '三体'})
MERGE (p)-[:ACTED_IN {role: '科学家'}]->(m);
MATCH (p:Person {name: '王五'}), (c:Company {name: '中影股份'})
MERGE (p)-[:WORKS_AT {position: '制片人'}]->(c);
MATCH (p:Person {name: '张三'}), (m:Movie {title: '满江红'})
MERGE (p)-[:REVIEWED {score: 8}]->(m);
// ============ 第六步:验证与分析 ============
// 6.1 全图概览(Browser 画布上会看到第一篇的"世界观"全景图)
MATCH (n) RETURN n;
// 6.2 张三的二度人脉(第一篇开头的"五层 JOIN 问题"的正面回答)
MATCH (:Person {name: '张三'})-[:KNOWS]->()-[:KNOWS]->(fof:Person)
RETURN DISTINCT fof.name AS 张三的二度人脉;
// 6.3 张三到赵六的最短路径
MATCH path = shortestPath(
(:Person {name: '张三'})-[*]-(:Person {name: '赵六'})
)
RETURN path;
// 6.4 每部电影参演人数排行
MATCH (m:Movie)<-[:ACTED_IN]-(p:Person)
RETURN m.title AS 电影, count(p) AS 演员数
ORDER BY 演员数 DESC;
// 6.5 社交达人榜(WITH + 聚合 + 过滤)
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WITH p, count(friend) AS friendCount
WHERE friendCount > 1
RETURN p.name AS 达人, friendCount AS 朋友数;

跑完 6.1,在画布上展开节点,第一篇的那张”数据模型全景图”就完整地立在了你的数据库里——三篇博客,一个场景,一条线走完。


6. 学习路线与进阶方向#

按 20/80 原则,以下内容等项目需要时再学,每项都有明确触发时机:

方向内容什么时候学
数据导入LOAD CSV 批量导入有真实历史数据要进图时
编程集成Python(neo4j 官方驱动)/ Java(Spring Data Neo4j)图数据库接入业务系统时
图算法PageRank、社区发现、路径分析(GDS 库)做推荐、风控、社交分析时
性能优化PROFILE / EXPLAIN 分析执行计划数据量到十万级节点以上时
知识图谱本体设计 → 图谱存储 → RAG 应用做大模型 + 知识库项目时

推荐资源#


7. 系列总结#

三篇博客,一个场景,回顾一下你走过的路:

  1. 概念篇:图数据库把关系当一等公民;节点、标签、关系、属性四大概念;Cypher 的画图思维
  2. CRUD 篇:CREATE 建点连线、MATCH 画模式查询、SET/REMOVE 改、DELETE/DETACH DELETE 删
  3. 进阶篇(本篇):MERGE 防重幂等、聚合排序统计、WITH 管道、索引约束双保险、完整实战收尾

一句话总结:Neo4j 的核心就是 “节点 + 关系 + 属性”,用 Cypher 的 MATCH 画模式、RETURN 取结果。掌握 CRUD、MERGE、多跳路径和聚合,就覆盖了 80% 的日常场景;剩下的 20%,在实际项目中按需补齐即可。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Neo4j 图数据库入门(三):从会写到写好——MERGE 防重、聚合分析、索引约束与完整实战
https://kianzhao.site/posts/Neo4j_Intermediate/
作者
Kian Zhao
发布于
2026-09-06
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Kian Zhao
Hello, I'm Kian Zhao.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
32
分类
8
标签
24
总字数
48,238
运行时长
0
最后活动
0 天前

目录