前两篇我们按关系型范式建模。但 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 建模一个实用心法:以业务聚合为单位组织文档,而非以”数据库表”为单位。边界划得好,既能享受嵌入的性能,又不至于单文档无限膨胀。
一个决策清单:该嵌入、引用还是换存储
遇到”这俩数据怎么存”,按这个顺序问:
- 它们是不是总一起被读取?是 → 嵌入(同一文档)。
- 子数据会不会无限增长(如评论几万条)?会 → 必须引用或拆集合。
- 子数据要不要被多方独立查询?要 → 引用(类似外键)。
- 关系是不是主角、要多次跳转?是 → 上图数据库。
- 是不是要强事务、跨实体一致?是 → 关系型更稳。
顺着这五个问题,大部分建模困惑都能落到具体方案。再次强调:没有”最正确”的模型,只有”最匹配访问模式”的模型。
小测验:看看你掌握了没
- 问题一:文档型嵌入和引用的取舍?答案:一起读、量小、不独立→嵌入;独立查、会膨胀、被多方引用→引用。
- 问题二:什么场景该用图数据库?答案:关系是主角、需多跳遍历(社交、推荐、风控)。
- 问题三:为什么 NoSQL 反而更需设计?答案:它把建模责任交给应用,冗余/拆分需按查询模式精心决策。
这一篇你该记住的
- 根本差异:关系型为”写一致”按范式建模;NoSQL 为”读性能”按查询建模,常主动冗余。
- 文档型:嵌入(一起读、量小)vs 引用(独立查、会膨胀);警惕无限增长数组。
- 图数据库:节点+边,关系即结构,擅长多跳遍历(社交/推荐/风控)。
- 三类存储各有所长,真实系统多用多模型混合按数据特征选型。
- 误区:NoSQL 更要设计、别一味嵌入、图库不擅聚合、别迷信单一数据库。
数据库三大系列(NoSQL / 调优 / 建模)到此收尾。接下来我们切换到安全领域,先补上密码学这一基石——它是一切安全机制的底层数学。