教程
🗄️

数据库

MySQL、Redis、MongoDB 等数据库原理与调优。

NoSQL 建模对比:文档与图的建模哲学

理解 NoSQL 与关系型建模的根本差异——关系型追求"减少冗余",文档型追求"查询最优",图数据追求"关系遍历",并掌握各自的设计套路。

· 更新于 2026-07-20 阅读量 --

前两篇我们按关系型范式建模。但 NoSQL 的建模哲学正好相反:关系型拼命”消除冗余”,NoSQL 常常”主动冗余”——因为目标不同。这一篇我们对比文档型(MongoDB)和图数据库(Neo4j)的建模思路,理解”为什么它们不按范式来”。

读完这篇,你能根据数据访问模式,决定”该冗余还是该拆分”。

核心差异:按”查询”而非”范式”建模

关系型建模问:“数据本身怎么组织才无冗余、无异常?“——答案是范式。

NoSQL 建模问:“我最常怎么查这些数据?“——然后围绕查询来组织,哪怕有点冗余。因为 NoSQL 没有 JOIN(或 JOIN 很弱),如果你还像关系型那样把数据拆得碎碎的,每次查询都要应用层拼装,会慢死。

一句话:关系型为”写一致”优化,NoSQL 为”读性能”优化

文档型(MongoDB)建模:嵌入 vs 引用

文档型最大的自由是嵌套。一个”博客文章+评论”该怎么存?两种套路:

套路一:嵌入(Embed)

把子数据直接嵌进父文档:

{
  "_id": 1,
  "title": "我的文章",
  "author": "小明",
  "comments": [
    { "user": "小红", "text": "好看" },
    { "user": "小刚", "text": "赞同" }
  ]
}

适合:一对少、子数据随父数据一起读、不独立查询的关系(如文章评论、用户地址)。一次读取全拿到,不用 JOIN。

套路二:引用(Reference)

子数据独立成集合,父文档只存对方 ID:

// 文章
{ "_id": 1, "title": "我的文章", "author": "小明", "comment_ids": [10, 11] }
// 评论集合
{ "_id": 10, "article_id": 1, "user": "小红", "text": "好看" }

适合:一对多且多端很大、或子数据要独立查询/被多处引用(如用户被很多文章引用)。这类似关系型外键。

怎么选:经验法则

  • 数据一起读、不独立、量不大 → 嵌入(快,一次取)。
  • 数据会单独查、可能无限增长、被多方引用 → 引用(避免单文档膨胀)。
  • 警惕无限增长数组:一篇文章评论几万条,全嵌进一个文档会超 16MB 限制且难查,必须改引用。

图数据库(Neo4j)建模:一切皆关系

关系型里”找 A 的朋友的朋友”要 JOIN 三次,数据大就崩。图数据库把关系当成一等公民

// 建两个人和一条"认识"关系
CREATE (a:Person {name:"小明"})-[:KNOWS]->(b:Person {name:"小红"})

// 查小明朋友的朋友
MATCH (a:Person {name:"小明"})-[:KNOWS*2]->(fof)
RETURN fof

[:KNOWS*2] 表示”走两步关系”,天然表达多跳。建模时你直接把实体当节点、关系当边,不用像关系型那样用中间表+JOIN 去”模拟”关系。

适合:社交网络、推荐系统、风控关系网、权限继承链。只要业务里”关系”是主角,图模型就比关系型直观得多。

对比一张表看清

维度关系型文档型图数据库
建模目标无冗余、一致读查询最优关系遍历最优
冗余态度消除(范式)主动冗余关系即结构
关联方式JOIN嵌入/引用遍历边
擅长事务、报表灵活结构、单文档聚合多跳关系
不擅长深层级关系跨文档聚合大规模聚合统计

混合架构:各取所长

真实系统往往是混合的:

  • 核心交易(订单、账目)→ 关系型,保事务一致。
  • 用户画像、商品详情、配置 → 文档型,结构灵活、读多。
  • 社交关系、推荐 → 图数据库。
  • 热点缓存 → Redis。

建模不再是”选一个数据库建模”,而是”按数据特征和访问模式,把不同数据放到最合适的存储”。这叫多模型(Polyglot Persistence)

常见认知误区

  • “NoSQL 不用设计,随便存”:恰恰相反,NoSQL 把建模责任从数据库转移到了你身上,设计更需想清访问模式,否则存出垃圾。
  • “文档型一律嵌入最爽”:嵌入无限增长数组会撑爆单文档、查询变慢。该引用就引用。
  • “图库能替代关系型做一切”:图库不擅长大规模聚合统计(如”本月总销售额”),这类还是关系型/数仓强。
  • “一种数据库打天下”:现代系统多用多模型混合,强行用单一存储会处处别扭。

宽列数据库(Cassandra/HBase)的建模

前面讲了文档和图,还有一类宽列(Wide-Column)库(如 Cassandra、HBase),建模思路和关系型完全不同:它先定义分区键(Partition Key)决定数据存在哪个节点,再定义聚类键(Clustering Key)决定分区内如何排序。建模时你要先想清楚”查询模式”,再倒推表结构:

-- 按用户查其最近 10 条动态:分区键=user_id,聚类键=create_time 倒序
CREATE TABLE user_feed (
  user_id UUID,
  create_time TIMESTAMP,
  content TEXT,
  PRIMARY KEY (user_id, create_time)
) WITH CLUSTERING ORDER BY (create_time DESC);

这里”一个用户的所有动态”在同一个分区,按时间排序,取最新 N 条极快。但如果你突然要”查全站热门动态”,这个表就帮不上忙——得另建一张按热度分区的表。这就是宽列库”一查询一表”的反范式哲学:为每种查询模式单独建模,用空间换读取速度。

时序数据建模

监控指标、IoT 传感器这类”带时间戳的海量数据点”,适合时序库(如 InfluxDB、TimescaleDB)或宽列库。建模要点:

  • 时间作为分区/分片维度,热数据近期、冷数据归档,查询天然按时间范围。
  • 把”标签/维度”(如机器名、指标名)作为索引列,把”数值”作为值,避免把每个指标建一张表。
  • 预聚合:原始点可能每秒百万级,查询时先 rollup 成分钟级/小时级汇总,存进汇总表。

这和关系型”一张大表存所有指标”的思路迥异,本质还是”按访问模式(按时间范围扫描)建模”。

聚合根:DDD 视角的 NoSQL 建模

领域驱动设计(DDD)提出**聚合根(Aggregate Root)**概念,和文档型建模天然契合:把”一起被修改、一起被加载”的数据聚成一个文档(一个聚合),以聚合根为单位存取。比如”订单”是聚合根,“订单项”是聚合内的子实体,整个订单作为一个文档读写,保证一致性边界清晰。

这给 NoSQL 建模一个实用心法:以业务聚合为单位组织文档,而非以”数据库表”为单位。边界划得好,既能享受嵌入的性能,又不至于单文档无限膨胀。

一个决策清单:该嵌入、引用还是换存储

遇到”这俩数据怎么存”,按这个顺序问:

  1. 它们是不是总一起被读取?是 → 嵌入(同一文档)。
  2. 子数据会不会无限增长(如评论几万条)?会 → 必须引用或拆集合。
  3. 子数据要不要被多方独立查询?要 → 引用(类似外键)。
  4. 关系是不是主角、要多次跳转?是 → 上图数据库。
  5. 是不是要强事务、跨实体一致?是 → 关系型更稳。

顺着这五个问题,大部分建模困惑都能落到具体方案。再次强调:没有”最正确”的模型,只有”最匹配访问模式”的模型

小测验:看看你掌握了没

  • 问题一:文档型嵌入和引用的取舍?答案:一起读、量小、不独立→嵌入;独立查、会膨胀、被多方引用→引用。
  • 问题二:什么场景该用图数据库?答案:关系是主角、需多跳遍历(社交、推荐、风控)。
  • 问题三:为什么 NoSQL 反而更需设计?答案:它把建模责任交给应用,冗余/拆分需按查询模式精心决策。

这一篇你该记住的

  • 根本差异:关系型为”写一致”按范式建模;NoSQL 为”读性能”按查询建模,常主动冗余。
  • 文档型:嵌入(一起读、量小)vs 引用(独立查、会膨胀);警惕无限增长数组。
  • 图数据库:节点+边,关系即结构,擅长多跳遍历(社交/推荐/风控)。
  • 三类存储各有所长,真实系统多用多模型混合按数据特征选型。
  • 误区:NoSQL 更要设计、别一味嵌入、图库不擅聚合、别迷信单一数据库。

数据库三大系列(NoSQL / 调优 / 建模)到此收尾。接下来我们切换到安全领域,先补上密码学这一基石——它是一切安全机制的底层数学。