【Neo4j 入门】为什么数据库还需要存关系

2388 字
12 分钟
【Neo4j 入门】为什么数据库还需要存关系

MySQL 存数据,ES 搜文档,Milvus 做语义匹配——三者存的都是「孤立的记录」,记录之间不认识。但现实世界的事物是有关系的:「珍珠奶茶」包含「珍珠」、「属于」「台式奶茶」、「适合」「学生」。Neo4j 就是用来存这种「节点 + 连线」的网络结构。这篇从零讲概念、写 Cypher、用 Node.js 做 CRUD,用一个奶茶知识图谱跑通所有基础操作。


一、你已经认识的数据库,各管什么#

先回顾一下你已经掌握的:

数据库存什么怎么查
MySQL表 + 行,精确匹配SELECT * WHERE name = '珍珠奶茶'
ES倒排索引 + 文档,词条匹配match { content: "珍珠" }
Milvus向量,语义相似度embedding.search("想喝甜的")

三者的共同特征:数据是孤立的。一条 ES 文档和另一条 ES 文档之间没有任何连线。

Neo4j 完全不同——它的数据模型天生就是节点 + 关系

(珍珠奶茶) --[包含]--> (珍珠)
(珍珠奶茶) --[包含]--> (红茶)
(珍珠奶茶) --[属于]--> (台式奶茶)
(珍珠) --[使用]--> (煮制)

存的是「事物之间的连接」,查的是「沿着连接走能找到什么」。


二、Neo4j 是什么#

图数据库,不是图片数据库#

Neo4j 是一个图数据库(Graph Database)

这里的”图”不是图片,是数学意义上的图论里的图——由节点(Node)边(Edge) 组成的网络结构。

现实世界里很多东西天然是图:

  • 社交网络:用户 → 关注 → 用户
  • 知识图谱:概念 → 属于 → 概念
  • 供应链:原材料 → 加工 → 产品 → 销售 → 客户
  • 推荐系统:用户 → 购买 → 商品 → 相似 → 商品

这些关系用 MySQL 存也能存,但查起来要写大量 JOIN,关系一深就慢。Neo4j 把「关系」当成一等公民直接存储,查关系和查属性一样快。

Neo4j 在数据库世界里的位置#

关系型数据库(MySQL / PostgreSQL)
→ 存结构化表格数据,擅长精确查询和事务
文档数据库(MongoDB)
→ 存 JSON 文档,擅长灵活 schema
搜索引擎(ElasticSearch)
→ 存倒排索引,擅长全文检索
向量数据库(Milvus / Pinecone)
→ 存高维向量,擅长语义相似度
图数据库(Neo4j) ← 今天的主角
→ 存节点 + 关系网络,擅长多跳关联查询

它们不是竞争关系,是各自擅长不同场景。实际项目里经常几个同时用。

查询语言:Cypher#

Neo4j 用 Cypher 作为查询语言,不是 SQL。

Cypher 的设计思路是”用 ASCII 画图”——语法长得就像图的样子:

-- SQL 的思路从哪张表取哪些列满足什么条件
SELECT i.name FROM products p JOIN contains c ON ... JOIN ingredients i ON ...
-- Cypher 的思路从哪个节点沿着什么关系走到哪个节点
MATCH (p:Product {name: "珍珠奶茶"})-[:包含]->(i:Ingredient)
RETURN i.name

Cypher 读起来更接近自然语言里描述关系的方式。学完这篇的基础操作,基本能看懂大多数 Cypher 查询。

可视化界面:Neo4j Browser#

Neo4j 自带一个 Web 界面——Neo4j Browser,默认跑在 http://localhost:7474

在里面直接写 Cypher,点运行,结果会以可视化图谱的形式展示出来——节点是圆圈,关系是带箭头的连线,可以拖拽、缩放、点击查看属性。

这是学 Neo4j 最直观的方式,建议边看这篇边开着 Browser 跑。


三、四个核心概念#

3.1 节点(Node)——就是图里的”实体”#

CREATE (p:Product {name: "珍珠奶茶", calorie: "中高"})
  • ( ) 圆括号 = 一个节点
  • p = 变量名,后面引用用
  • :Product = 标签(Label),相当于「类型」
  • { } = 属性(Property),键值对

3.2 关系(Relationship)——就是”连线”#

MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "珍珠"})
CREATE (p)-[:包含]->(i)
  • -[:包含]-> = 一条有名字、有方向的连线
  • 方向很重要:(A)-[:属于]->(B)(A)<-[:属于]-(B) 是两条不同的关系

3.3 属性(Property)——节点和关系都可以有#

CREATE (p:Product {name: "珍珠奶茶", price: 15})
CREATE (p)-[:包含 {quantity: "适量"}]->(i)

3.4 标签(Label)——一个节点可以有多个标签#

CREATE (p:Product:Beverage:HotItem {name: "珍珠奶茶"})

这就像给节点打了三个分类标签。


四、用奶茶知识图谱理解「多跳查询」为什么强#

我们在 Neo4j 里建一个奶茶知识图谱,五个节点类型:

Product(奶茶产品)
├─[属于]→ Type(奶茶类型:台式、港式)
├─[包含]→ Ingredient(配料:珍珠、果糖、红茶、牛奶)
└─[适合]→ People(人群:年轻人、学生、甜食爱好者)
Ingredient
└─[使用]→ Method(工艺:煮制、冲泡)

一条 Cypher 串起三层关系:

// 珍珠奶茶 → 配料 → 制作工艺(两步跳)
MATCH (p:Product {name: "珍珠奶茶"})-[:包含]->(i)-[:使用]->(m)
RETURN p.name, i.name, m.name
结果:
珍珠奶茶 | 珍珠 | 煮制

这在 MySQL 里需要三个 JOIN,关系再深一层就指数级变慢。 Neo4j 的 [*1..3] 语法可以一次跳多步,遍历深度不影响性能。


五、Cypher 基础操作:CRUD + MERGE#

5.1 CREATE — 创建节点#

CREATE (p:Product {name: "珍珠奶茶"})
CREATE (t1:Type {name: "台式奶茶"})
CREATE (t2:Type {name: "港式奶茶"})
CREATE (i1:Ingredient {name: "珍珠"})
CREATE (i2:Ingredient {name: "果糖"})
CREATE (i3:Ingredient {name: "红茶"})
CREATE (i4:Ingredient {name: "牛奶"})
CREATE (m1:Method {name: "煮制"})
CREATE (m2:Method {name: "冲泡"})
CREATE (peo1:People {name: "年轻人"})
CREATE (peo2:People {name: "学生"})
CREATE (peo3:People {name: "甜食爱好者"})

5.2 创建关系#

// 珍珠奶茶 属于 台式奶茶
MATCH (p:Product {name: "珍珠奶茶"}), (t:Type {name: "台式奶茶"})
CREATE (p)-[:属于]->(t)
// 珍珠奶茶 包含 配料
MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "珍珠"})
CREATE (p)-[:包含]->(i)
-- 同理对果糖红茶牛奶各执行一次
// 珍珠 使用 煮制工艺
MATCH (i:Ingredient {name: "珍珠"}), (m:Method {name: "煮制"})
CREATE (i)-[:使用]->(m)
// 珍珠奶茶 适合 人群
MATCH (p:Product {name: "珍珠奶茶"}), (peo:People {name: "年轻人"})
CREATE (p)-[:适合]->(peo)
-- 同理对学生甜食爱好者各执行一次

5.3 MATCH — 查询#

-- 单跳珍珠奶茶有哪些配料
MATCH (p:Product {name: "珍珠奶茶"})-[:包含]->(i:Ingredient)
RETURN i.name
-- 多跳珍珠奶茶的配料用什么工艺
MATCH (p:Product {name: "珍珠奶茶"})-[:包含]->(i)-[:使用]->(m)
RETURN p.name, i.name, m.name
-- 查所有节点和关系可视化
MATCH (n)-[r]->(m)
RETURN n, r, m

5.4 SET — 更新属性#

MATCH (p:Product {name: "珍珠奶茶"})
SET p.price = 15, p.calorie = "中高"

5.5 DELETE — 删除#

-- 删除关系
MATCH (p:Product {name: "珍珠奶茶"})-[r:包含]->(i:Ingredient {name: "珍珠"})
DELETE r
-- 删除节点不能有残留关系
MATCH (i:Ingredient {name: "芋圆"})-[r]-()
DELETE r, i

必须先删除关系再删除节点,Neo4j 不允许存在无归属的关系。

5.6 ⚠️ CREATE vs MERGE(新手第一个坑)#

这是 Neo4j 新手最容易犯的错。看一下这段代码:

-- 每次执行都创建一条新关系不管是否已存在
MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "果糖"})
CREATE (p)-[:包含]->(i)
-- 执行 2 = 2 条重复的包含果糖关系

解决方案:用 MERGE

-- 不存在就创建已存在就跳过
MERGE (p:Product {name: "珍珠奶茶"})
MERGE (i:Ingredient {name: "果糖"})
MERGE (p)-[:包含]->(i)
-- 执行多少次都只有 1 条关系

类比你已学过的:

ESMilvusNeo4j
upsertupsertMERGE

三者都是「幂等写入」——重复执行不会产生脏数据。

记忆规则:探索阶段用 CREATE(快但可能重复),生产代码用 MERGE(安全但稍慢)。


六、Node.js 代码操作 Neo4j#

6.1 连接数据库#

import neo4j from 'neo4j-driver';
const driver = neo4j.driver(
'bolt://localhost:7687',
neo4j.auth.basic('neo4j', '12345678')
);
const session = driver.session();

对比 @elastic/elasticsearch@zilliz/milvus2-sdk-node——连接方式思想相同,只是协议不同。

6.2 完整的 CRUD 封装#

// 创建节点
async function createData() {
await session.run(`
MERGE (p:Product {name: "珍珠奶茶"})
MERGE (i:Ingredient {name: "珍珠"})
`);
}
// 创建关系
async function createRelation() {
await session.run(`
MATCH (p:Product {name: "珍珠奶茶"}), (i:Ingredient {name: "珍珠"})
MERGE (p)-[:包含]->(i)
`);
}
// 查询
async function queryData() {
const result = await session.run(`
MATCH (p:Product {name: "珍珠奶茶"})-[r]->(i)
RETURN p, r, i
`);
result.records.forEach(record => {
console.log('奶茶:', record.get('p').properties.name);
console.log('关系:', record.get('r').type);
console.log('目标:', record.get('i').properties.name);
});
}
// 更新属性
async function updateData() {
await session.run(`
MATCH (p:Product {name: "珍珠奶茶"})
SET p.price = 15, p.calorie = "中高"
`);
}
// 清理重复关系(按 id 排序,保留最早的一条)
async function deleteDuplicateRelation() {
await session.run(`
MATCH (p:Product {name: "珍珠奶茶"})-[r:包含]->(i:Ingredient {name: "果糖"})
WITH r ORDER BY id(r)
SKIP 1
DELETE r
`);
}
// 用完后关闭连接
async function main() {
try {
await queryData();
} finally {
await session.close();
await driver.close();
}
}
main();

6.3 关键注意点#

✅ driver → session → run → close(用完要关,防止连接泄漏)
✅ record.get('i').properties.name(取属性的正确写法)
✅ MERGE 代替 CREATE(幂等安全)
⚠️ SKIP 前必须 ORDER BY(Neo4j 的语法要求)

七、Docker 安装#

Terminal window
cd examples/neo4j-graphrag
docker compose up -d
# 浏览器打开 http://localhost:7474
# 用户名 neo4j / 密码 12345678

直接在 Neo4j Browser 的输入框里写 Cypher 语句,点运行按钮就能看到可视化图谱——节点是圆圈,关系是带箭头的连线,可以拖拽、缩放、点击查看属性。这是 Neo4j 最直观的学习方式。


八、小结#

创建节点 → CREATE / MERGE
查询 → MATCH ... RETURN
更新属性 → SET
删除关系 → DELETE r
删除节点 → DELETE r, i(必须先断关系)
幂等写入 → MERGE(不会重复创建)
清理重复 → ORDER BY + SKIP + DELETE
Node.js 端:
neo4j-driver → driver → session → session.run()

Neo4j 本质上不是在和「表」打交道,而是在和「图」打交道。理解了这个思维转变,后面写 GraphRAG 就水到渠成了。


昇哥 · 2026年7月

支持与分享

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

【Neo4j 入门】为什么数据库还需要存关系
https://blog.fridolph.top/posts/2026-06-15__neo4j-crud-basics/
作者
Fridolph
发布于
2026-07-04
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录