【图数据库实战进阶(03):Neo4j 图数据建模实战——节点、关系与属性应该怎么设计?】

TL;DR

  • 系列方向:以 Neo4j + Cypher 为核心,从图数据库基础开始,逐步完成用户关系、好友推荐、商品关联等业务项目。
  • 本篇重点:掌握图数据库的数据建模方法,理解节点、关系、属性分别应该承载什么信息,并通过用户社交关系案例设计一套可扩展的数据模型。

前两篇我们已经完成了 Neo4j 环境搭建,也接触了 Cypher 的基本操作。

但真正开始做业务时,很快就会遇到一个问题:

业务对象应该设计成节点,还是属性?两个对象之间的联系应该设计成关系,还是直接存储在节点属性里?

这不是语法问题,而是图数据库建模中更重要的一步。

如果模型设计不合理,后面写查询时就会越来越复杂,甚至需要推翻原来的结构重新设计。

这一篇就通过一个用户社交关系场景,把图数据模型从头设计一遍。


一、先看一个实际业务场景

假设我们正在开发一个用户社交系统,需要支持以下功能:

  1. 查询用户的基本信息。
  2. 查询某个用户的好友。
  3. 查询两个用户之间是否存在好友关系。
  4. 查询两个用户之间的共同好友。
  5. 查询用户关注了哪些人。
  6. 后续增加好友推荐功能。

如果使用传统关系型数据库,我们可能会设计以下几张表:

  • user:用户信息表
  • user_friend:用户好友关系表
  • user_follow:用户关注关系表

例如:

用户表

user_id username age
1001 张三 25
1002 李四 28
1003 王五 26

好友关系表

user_id friend_id
1001 1002
1001 1003
1002 1003

这种设计没有问题。

但当业务开始增加多层关系查询时,我们需要不断通过关联查询来寻找用户之间的联系。

在图数据库中,我们可以换一种思路:

  • 用户是节点。
  • 好友关系是关系。
  • 关注关系也是关系。
  • 用户姓名、年龄等信息作为节点属性。

最终形成一张用户关系网络。

二、节点、关系、属性分别应该放什么?

Neo4j 使用属性图模型,主要由三个部分组成。

1. 节点:表示业务中的实体

节点通常代表一个具有独立业务意义的对象。

例如:

  • 用户
  • 商品
  • 订单
  • 公司
  • 设备
  • 文章

在我们的社交系统中,一个用户就是一个节点。

cypher 复制代码
CREATE (u:User {
    user_id: '1001',
    username: '张三',
    age: 25,
    city: '上海'
});

这里的 User 是标签,表示节点属于用户类型。

节点标签可以帮助我们区分不同业务对象。

例如:

cypher 复制代码
CREATE (u:User {
    user_id: '1002',
    username: '李四'
});

CREATE (p:Product {
    product_id: 'P001',
    name: '机械键盘',
    price: 299
});

虽然两者都是节点,但它们代表完全不同的业务实体。

实际项目中,我更建议按照业务对象划分节点标签,而不是把所有数据都塞进一个通用节点类型。

2. 关系:表示实体之间的联系

图数据库最重要的特点之一,就是关系可以直接参与查询。

例如:

张三和李四是好友:

cypher 复制代码
MATCH (a:User {user_id: '1001'}),
      (b:User {user_id: '1002'})
CREATE (a)-[:FRIEND]->(b);

张三关注了王五:

cypher 复制代码
MATCH (a:User {user_id: '1001'}),
      (b:User {user_id: '1003'})
CREATE (a)-[:FOLLOWS]->(b);

这里有两个不同的关系类型:

  • FRIEND:好友关系。
  • FOLLOWS:关注关系。

需要注意一个建模细节:

好友关系和关注关系不应该随意混用。

如果业务中的好友关系是双向的,那么只存储一条有向关系时,查询双方好友都应该明确处理关系方向。例如,可以统一采用 user_id 较小的一方指向较大的一方作为存储规范,再通过无向匹配查询:

cypher 复制代码
MATCH (a:User)-[:FRIEND]-(b:User)
RETURN a.username, b.username;

如果关注关系是单向的,则应该保留方向:

cypher 复制代码
MATCH (a:User)-[:FOLLOWS]->(b:User)
RETURN a.username, b.username;

不要因为两者都是用户之间的联系,就把它们设计成同一种关系。

3. 属性:描述节点或关系的特征

属性主要用于描述实体本身,或者描述一段关系。

用户节点可以保存:

cypher 复制代码
{
    user_id: '1001',
    username: '张三',
    age: 25,
    city: '上海'
}

关系也可以有属性。

例如,好友关系建立时间:

cypher 复制代码
MATCH (a:User {user_id: '1001'}),
      (b:User {user_id: '1002'})
CREATE (a)-[:FRIEND {
    created_at: '2026-01-10',
    source: 'mobile_app'
}]->(b);

这样就可以记录关系的业务信息。

查询时:

cypher 复制代码
MATCH (a:User)-[r:FRIEND]-(b:User)
RETURN
    a.username,
    b.username,
    r.created_at,
    r.source;

这里有一个值得注意的地方:

如果某个字段描述的是用户自身,就应该优先考虑放在用户节点上;如果描述的是两个用户之间的联系,就应该考虑放在关系上。

例如:

数据 推荐存储位置
用户姓名 User 节点属性
用户年龄 User 节点属性
用户所在城市 User 节点属性
好友建立时间 FRIEND 关系属性
关注时间 FOLLOWS 关系属性
关注来源 FOLLOWS 关系属性

三、实际项目中,应该如何设计图模型?

现在我们开始设计一个稍微完整一点的用户社交模型。

业务需求如下:

  • 用户之间可以建立好友关系。
  • 用户可以关注其他用户。
  • 用户可以发布文章。
  • 用户可以给文章打标签。
  • 用户之间可以通过共同兴趣发现潜在好友。

先不要急着写 Cypher,先把业务对象梳理出来。

1. 确定节点类型

节点标签 代表对象 主要属性
User 用户 user_id、username、age、city
Article 文章 article_id、title、created_at
Tag 标签 tag_id、name

这里为什么要把 Tag 单独设计成节点?

假设用户发布的文章有多个标签:

  • Java
  • Python
  • 数据库
  • 软件测试

如果标签只是文章的一个普通字符串属性,那么后续想查询共同兴趣、标签下的文章、对某个标签感兴趣的用户,就需要额外处理大量字符串。

把标签设计成节点后,就能通过关系表达它们之间的联系。

2. 确定关系类型

关系类型 起点 终点 业务含义
FRIEND User User 用户好友关系
FOLLOWS User User 用户关注关系
PUBLISHES User Article 用户发布文章
HAS_TAG Article Tag 文章拥有标签
INTERESTED_IN User Tag 用户对标签感兴趣

最终模型可以理解为:

text 复制代码
(User)-[:FRIEND]-(User)

(User)-[:FOLLOWS]->(User)

(User)-[:PUBLISHES]->(Article)

(Article)-[:HAS_TAG]->(Tag)

(User)-[:INTERESTED_IN]->(Tag)

这张图已经可以支持不少业务查询。

例如:

  • 查询用户发布的文章。
  • 查询文章所属标签。
  • 查询喜欢相同标签的用户。
  • 查询好友之间的共同兴趣。
  • 根据共同兴趣推荐潜在好友。

3. 用 Cypher 创建模型

下面把模型转换成实际数据。

首先创建用户:

cypher 复制代码
CREATE
(:User {
    user_id: '1001',
    username: '张三',
    age: 25,
    city: '上海'
}),
(:User {
    user_id: '1002',
    username: '李四',
    age: 28,
    city: '北京'
}),
(:User {
    user_id: '1003',
    username: '王五',
    age: 26,
    city: '上海'
});

创建文章:

cypher 复制代码
CREATE
(:Article {
    article_id: 'A001',
    title: 'Neo4j 入门实践',
    created_at: '2026-11-01'
}),
(:Article {
    article_id: 'A002',
    title: 'Python 自动化测试',
    created_at: '2026-11-02'
});

创建标签:

cypher 复制代码
CREATE
(:Tag {tag_id: 'T001', name: 'Neo4j'}),
(:Tag {tag_id: 'T002', name: 'Python'}),
(:Tag {tag_id: 'T003', name: '软件测试'});

建立用户之间的关系:

cypher 复制代码
MATCH
    (u1:User {user_id: '1001'}),
    (u2:User {user_id: '1002'}),
    (u3:User {user_id: '1003'})
CREATE
    (u1)-[:FRIEND]->(u2),
    (u1)-[:FRIEND]->(u3),
    (u1)-[:FOLLOWS]->(u2);

建立文章和用户的关系:

cypher 复制代码
MATCH
    (u:User {user_id: '1001'}),
    (a:Article {article_id: 'A001'})
CREATE (u)-[:PUBLISHES]->(a);

建立文章和标签的关系:

cypher 复制代码
MATCH
    (a:Article {article_id: 'A001'}),
    (t:Tag {tag_id: 'T001'})
CREATE (a)-[:HAS_TAG]->(t);

用户兴趣关系:

cypher 复制代码
MATCH
    (u1:User {user_id: '1001'}),
    (u2:User {user_id: '1002'}),
    (t1:Tag {tag_id: 'T001'}),
    (t2:Tag {tag_id: 'T002'})
CREATE
    (u1)-[:INTERESTED_IN]->(t1),
    (u2)-[:INTERESTED_IN]->(t1),
    (u2)-[:INTERESTED_IN]->(t2);

现在,我们就有了一张包含用户、文章、标签以及它们之间关系的图。


四、模型设计完成后,如何验证它是否合理?

模型不是画出来就结束了。

我更建议在正式开发之前,先用几个真实业务问题验证模型。

如果模型需要写很多复杂查询才能完成简单业务,通常意味着还需要重新检查设计。

场景一:查询用户发布的文章

cypher 复制代码
MATCH (u:User {user_id: '1001'})
      -[:PUBLISHES]->(a:Article)
RETURN
    u.username,
    a.title,
    a.created_at;

场景二:查询文章所属标签

cypher 复制代码
MATCH (a:Article {article_id: 'A001'})
      -[:HAS_TAG]->(t:Tag)
RETURN
    a.title,
    collect(t.name) AS tags;

场景三:查询具有相同兴趣的用户

cypher 复制代码
MATCH
    (u1:User {user_id: '1001'})
      -[:INTERESTED_IN]->(t:Tag)
      <-[:INTERESTED_IN]-
    (u2:User)
WHERE u1 <> u2
RETURN
    u2.username,
    collect(t.name) AS common_tags;

这个查询可以找到与张三具有相同兴趣标签的其他用户。

如果后续要做好友推荐,就可以在此基础上增加好友排除、兴趣数量统计、关系权重等业务规则。

场景四:查询好友之间的共同兴趣

cypher 复制代码
MATCH
    (u1:User {user_id: '1001'})
      -[:FRIEND]-(u2:User),
    (u1)-[:INTERESTED_IN]->(t:Tag),
    (u2)-[:INTERESTED_IN]->(t)
RETURN
    u2.username,
    collect(t.name) AS common_tags;

这类查询体现了图数据库的优势:不需要手动维护多张关系表的关联条件,而是沿着已经定义好的关系进行查询。


五、图数据库建模中容易踩的几个坑

这部分比记住几个 Cypher 语法更重要。

坑一:把所有信息都设计成节点

并不是所有数据都适合单独创建节点。

例如用户年龄、手机号、注册时间等,通常只是用户自身的属性,没有必要全部独立成节点。

判断标准可以简单一些:

如果一个对象具有独立的业务身份,并且需要被多个对象引用或建立关系,那么可以考虑设计为节点。

如果只是描述对象的一个特征,通常放在属性中就够了。

坑二:把关系信息全部放进节点属性

例如:

cypher 复制代码
CREATE (:User {
    username: '张三',
    friends: ['李四', '王五', '赵六']
});

这种设计看起来简单,但后续会带来问题:

  • 无法直接表达好友之间的关系属性。
  • 查询共同好友需要额外处理数组。
  • 好友关系变化时,需要更新节点内部的数据。
  • 多层关系查询不够自然。

如果好友本身是独立用户,应该优先使用关系表达:

text 复制代码
(张三)-[:FRIEND]-(李四)

坑三:关系方向没有统一规范

Neo4j 的关系具有方向。

如果同一种业务关系有时从 A 指向 B,有时从 B 指向 A,后续查询就容易出现遗漏。

例如好友关系是双向的,但系统中只存储一条关系,那么查询时需要使用无向模式:

cypher 复制代码
MATCH (a:User)-[:FRIEND]-(b:User)
RETURN a, b;

如果业务要求分别记录双方的好友状态,或者双方可以独立解除关系,就需要进一步设计状态和关系语义,不能简单认为一条关系就能解决所有问题。

坑四:没有设计业务唯一标识

不要直接把 Neo4j 内部节点 ID 当作业务用户 ID。

业务系统应该使用稳定的业务标识,例如:

  • user_id
  • product_id
  • order_id

并为关键标识创建唯一约束:

cypher 复制代码
CREATE CONSTRAINT user_id_unique
FOR (u:User)
REQUIRE u.user_id IS UNIQUE;

这样可以避免同一个业务用户被重复创建。

坑五:只考虑当前查询,不考虑未来扩展

例如一开始只需要查询好友,直接设计 FRIEND 关系就可以。

但后续可能增加:

  • 好友申请状态。
  • 好友建立时间。
  • 关系来源。
  • 亲密度。
  • 是否屏蔽。

这些信息可能需要放到关系属性中,也可能需要进一步引入独立的关系实体节点,具体取决于业务复杂度。

不要为了未来所有可能性过度设计,但也不要把明显属于关系本身的信息随意塞进用户节点。


六、本篇总结

这一篇我们完成了一个用户社交系统的基础图模型设计。

回顾一下:

  1. 节点:表达用户、文章、标签等独立业务实体。
  2. 关系:表达用户之间、用户与文章之间的业务联系。
  3. 属性:描述节点或关系自身的特征。
  4. 关系方向:需要根据业务语义统一设计。
  5. 业务唯一标识:不要依赖内部节点 ID 代替业务主键。

我个人认为,图数据库学习中最值得花时间的地方并不是 Cypher 语法,而是建模。

语法写错了可以修改,模型设计错了,后面可能连查询方式和业务实现都要跟着调整。

因此,在实际项目中,我一般会先列出业务实体、梳理实体之间的关系,再用几个真实业务查询验证模型,最后才开始完善代码。

相关推荐
瑞克-桑切斯20 分钟前
redis
数据库·redis·缓存
freejackman1 小时前
【工程化】从“单兵作战”到“多引擎架构”:基于门面模式的重构实战
大数据·数据库·hive·sql·测试工具·设计模式·架构
bksczm2 小时前
InnoDB 内存结构总览:Change Buffer、AHI 与 Log Buffer
数据库·mysql
花间相见2 小时前
【计算基础|Redis02】【上手】五大数据类型命令逐条实操
数据库·redis·后端
敲上瘾3 小时前
Qt Widgets入门:项目模板解析与Hello World
开发语言·数据库·c++·qt·ui·qt6.3
bksczm4 小时前
Redis 持久化:RDB 快照与 AOF 日志
数据库·redis·mybatis
.痞老板.4 小时前
mysql初阶--数据库设计
数据库·mysql·oracle
Decay__6 小时前
SQL Server 数据库全栈运维实战系列 — 合集索引(24 篇)
运维·数据库·sql·mysql·架构
Thanks_ks6 小时前
SQL 的 GROUP BY 与聚合函数:从会写 SELECT 到会做统计
数据库·sql·mysql·group by·数据统计·聚合函数·having