这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 MongoDB 的基本数据单位是 BSON 文档,文档组成集合,集合组成数据库。文档模型的价值不只是“字段灵活”,而是让一组经常一起读取的数据可以放在一个边界内,从而减少 JOIN 和多次查询。
3.1 数据层级
Database
Collection
Document
Field
示例:
use shop
db.products.insertOne({
sku_id: "sku_10001",
title: "轻薄笔记本",
price: NumberDecimal("5999.00"),
attrs: {
cpu: "16核",
memory: "32GB"
},
tags: ["轻薄", "高续航"],
status: "ON_SALE",
created_at: new Date()
})
3.2 _id 与 ObjectId
每个文档必须有 _id,集合内唯一。
若插入时没有提供,MongoDB 默认生成 ObjectId:
{
_id: ObjectId("66cb1f0b8d0f4e51f0a8b123")
}
ObjectId 通常包含时间戳信息,但不应把它当成业务时间,业务仍应显式保存 created_at。
业务也可以使用自然键:
db.orders.insertOne({
_id: "o_20260825_000001",
status: "CREATED"
})
3.3 常用 BSON 类型
| 类型 | 示例 |
|---|---|
| String | "Alice" |
| Int32 / Int64 | NumberLong("100") |
| Double | 1.5 |
| Decimal128 | NumberDecimal("19.90") |
| Boolean | true |
| Date | new Date() |
| ObjectId | ObjectId() |
| Array | ["a", "b"] |
| Object | { city: "Shanghai" } |
| Null | null |
金额使用 Decimal128,计数使用整数,时间使用 Date。不要用字符串保存时间和数字。
3.4 字段名设计
字段名会保存在每个文档中,过长字段名会增加存储和网络成本。但不应为了省空间使用不可读的缩写。
推荐:
{
user_id: "u_1001",
order_no: "O202608250001",
total_amount: NumberDecimal("199.00"),
created_at: new Date()
}
避免:
{
uid: "u_1001",
ono: "O202608250001",
amt: 199
}
团队应统一命名规范,例如 snake_case 或 camelCase,并在应用对象映射层保持一致。
3.5 嵌套文档
适合表示从属关系:
{
order_no: "O202608250001",
status: "PAID",
shipping_address: {
country: "中国",
city: "上海",
street: "南京西路 100 号"
},
items: [
{ sku_id: "sku_10001", quantity: 1 }
]
}
优点:
- 一次读取完整订单;
- 地址与订单生命周期一致;
- 单文档事务保证;
- 避免频繁 join。
限制:
- 无界增长;
- 高频局部更新;
- 超大文档;
- 多方共享修改。
3.6 数组建模
数组可以表达标签、明细、权限等:
{
article_id: "a_100",
title: "MongoDB 设计",
tags: ["mongodb", "database"],
comments_count: 120
}
数组适合有限且随主文档读取的数据。订单明细若持续增长,需要评估拆分;评论量大时通常拆成独立集合并分页查询。
3.7 引用建模
用户与订单示例:
db.users.insertOne({
_id: "u_1001",
name: "Alice",
level: "GOLD"
})
db.orders.insertOne({
_id: "o_10001",
user_id: "u_1001",
amount: NumberDecimal("299.00"),
status: "PAID"
})
查询订单并关联用户:
db.orders.aggregate([
{ $match: { _id: "o_10001" } },
{ $lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user"
}},
{ $unwind: "$user" }
])
$lookup 方便,但高频关联查询通常说明模型需要调整或使用冗余字段。
3.8 Schema 灵活性
同一集合中的文档可以有不同字段:
db.products.insertOne({ sku_id: "A", title: "A" })
db.products.insertOne({ sku_id: "B", title: "B", color: "red" })
这不是“没有 Schema”,而是 Schema 由应用和 validator 约束。
集合级验证示例:
db.runCommand({
collMod: "products",
validator: {
$jsonSchema: {
required: ["sku_id", "title"],
properties: {
sku_id: { bsonType: "string", minLength: 1 },
price: { bsonType: "decimal", minimum: 0 }
}
}
},
validationLevel: "strict",
validationAction: "error"
})
3.9 文档大小与工作集
单个 BSON 文档有大小上限,历史上常见为 16MB。生产设计应让常用文档远小于该值。
过大文档的问题:
- 读写放大;
- 内存压力大;
- 更新冲突;
- 复制延迟增加;
- 网络传输浪费。
处理方式:
- 拆分历史明细;
- 大文本或图片放对象存储;
- 低频字段独立集合;
- 列表分页;
- 使用覆盖常用字段的投影。
3.10 建模原则
- 从查询和更新模式出发;
- 一起读的数据放一起;
- 避免无界数组;
- 高频更新计数可单独集合;
- 金额、时间、ID 类型要明确;
- 常用查询要有索引支撑;
- 事务边界尽量小;
- 分片可能性要提前评估;
- 集合职责清晰,不做万能大杂烩;
- 应用契约与 validator 双重约束。
本章小结
文档模型通过嵌套和数组减少不必要的关联,让数据形态接近业务读取方式。但灵活不等于随意,必须控制文档大小、数组增长、字段类型和更新频率。好的 MongoDB 设计通常先列出查询和写入模式,再决定内嵌、引用或混合建模。
思考题
- 为什么金额应使用 Decimal128?
- 无界数组会带来哪些风险?
- 什么时候应该拆分集合?
$lookup高频出现说明什么?- validator 能替代应用层校验吗?