【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.nameCypher 读起来更接近自然语言里描述关系的方式。学完这篇的基础操作,基本能看懂大多数 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, m5.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 条关系类比你已学过的:
| ES | Milvus | Neo4j |
|---|---|---|
upsert | upsert | MERGE |
三者都是「幂等写入」——重复执行不会产生脏数据。
记忆规则:探索阶段用 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 安装
cd examples/neo4j-graphragdocker 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月
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!