2. 文档模型核心概念
MongoDB 的数据组织方式与关系型数据库有本质差异。理解这些差异,是后续做数据模型设计的前提。本节先把文档、集合、数据库、BSON、内嵌文档、数组这几个核心概念讲清楚,再通过对比关系型模型,说明反范式化设计思想到底在解决什么问题。
一、五个核心概念
文档(Document)是 MongoDB 中最小的数据单元,等价于关系型数据库中的“行”,但结构完全不同。一个文档是一组键值对的有序集合,用类似 JSON 的形式书写。例如一条用户记录:
{
_id: ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
name: "张伟",
age: 28,
city: "杭州",
tags: ["后端", "MongoDB", "分布式"]
}
每个文档都有一个 _id 字段作为主键,若插入时未指定,驱动会自动生成一个 ObjectId。文档的键是字符串,值可以是字符串、数字、布尔、日期、数组、甚至另一个文档。
集合(Collection)是文档的容器,对应关系型数据库中的“表”。但集合不强制固定结构:同一个集合里的文档可以有不同字段、不同字段数量。这带来灵活性,也意味着结构约束需要由应用层或 Schema 校验来保证。
数据库(Database)是集合的容器,一个 MongoDB 实例可以承载多个数据库,每个数据库拥有独立的集合与权限。典型层级是:实例 → 数据库 → 集合 → 文档。
BSON 是 MongoDB 实际存储文档使用的二进制格式,全称 Binary JSON。它在 JSON 基础上扩展了更多数据类型,例如 ObjectId、Date、Decimal128、Binary、正则等,并带有长度前缀,便于快速遍历和定位字段。我们书写时用 JSON 风格,落盘和网络传输时是 BSON。
内嵌文档与数组是文档模型区别于关系模型的关键能力。内嵌文档指一个字段的值本身又是一个文档;数组指一个字段的值是一组值或一组文档。它们让“一对多”“多对多”关系可以直接放进一个文档里,而不必拆成多张表。
二、与关系型模型的关键差异
把上面的层级图对照来看,差异集中在三点。
第一,结构约束不同。关系型表在建模时就固定了列,插入数据必须符合列定义;MongoDB 集合不强制字段一致,同一集合可以容纳结构不同的文档。这既是灵活性,也要求设计者主动管理结构一致性。
第二,关系表达方式不同。关系型用外键把数据拆到多张表,查询时靠 JOIN 拼回;MongoDB 允许把关联数据直接内嵌进同一个文档,一次读取即可拿到完整对象。例如上面的用户文档,把地址内嵌为 address 子文档,把标签放进 tags 数组,就不需要额外的 addresses 表和 tags 表。
第三,读写模式不同。关系型强调规范化,减少冗余;MongoDB 允许适度冗余,用空间换读取效率。这就是反范式化设计思想的核心。
三、反范式化设计思想
范式化(Normalization)的目标是消除冗余、保证一致性,代价是查询时需要 JOIN。反范式化(Denormalization)则主动引入冗余,把经常一起读取的数据放在同一个文档里,用一次查询替代多次关联。
判断是否内嵌,可以看三个问题:
- 读取频率:子数据是否几乎总是随主数据一起被读取?是,则倾向内嵌。
- 数量规模:子数据数量是否有限且可控?例如一个用户的收货地址通常几个到几十个,适合内嵌;如果是订单流水,可能无限增长,则应独立成集合。
- 更新模式:子数据是否独立频繁更新?若独立更新频繁,内嵌会导致文档反复重写,此时引用更合适。
下面给出一个可参考的判断表,用于在“内嵌”与“引用”之间做选择:
| 判断维度 | 倾向内嵌 | 倾向引用 |
|---|---|---|
| 读取模式 | 总是一起读 | 经常单独读 |
| 子文档数量 | 有限、可预估 | 无限增长 |
| 更新频率 | 随主文档一起更新 | 独立高频更新 |
| 一致性要求 | 强一致、同文档原子 | 可接受最终一致 |
| 典型例子 | 用户地址、订单明细 | 用户与订单、评论与文章 |
四、一个建模示例
假设要做电商订单。关系型做法是 orders 表 + order_items 表,查询订单详情要 JOIN。文档模型可以这样设计:
{
_id: ObjectId("..."),
orderNo: "SO20240521001",
userId: ObjectId("..."), // 引用用户,用户独立高频更新
status: "paid",
items: [ // 内嵌订单明细,总是一起读
{ sku: "A1001", name: "机械键盘", qty: 1, price: 399.00 },
{ sku: "B2003", name: "鼠标垫", qty: 2, price: 29.00 }
],
totalAmount: 457.00,
createdAt: ISODate("2024-05-21T10:30:00Z")
}
这里 userId 用引用,因为用户信息会被独立频繁修改;items 用内嵌数组,因为订单明细永远随订单一起读取,且数量有限。这就是反范式化思想在实际建模中的落地:不是所有关系都内嵌,也不是所有关系都引用,而是按读写模式分别决策。
掌握这五个概念和反范式化的判断逻辑后,后续的 Schema 设计、索引设计和查询优化才有稳定的出发点。文档模型不是“把表换成 JSON”,而是围绕“数据如何被读取”重新组织数据。