MongoDBNotes

第 03 章:文档模型

zjc 于 2026-01-03 发布

这是《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 }
  ]
}

优点:

  1. 一次读取完整订单;
  2. 地址与订单生命周期一致;
  3. 单文档事务保证;
  4. 避免频繁 join。

限制:

  1. 无界增长;
  2. 高频局部更新;
  3. 超大文档;
  4. 多方共享修改。

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。生产设计应让常用文档远小于该值。

过大文档的问题:

  1. 读写放大;
  2. 内存压力大;
  3. 更新冲突;
  4. 复制延迟增加;
  5. 网络传输浪费。

处理方式:

  1. 拆分历史明细;
  2. 大文本或图片放对象存储;
  3. 低频字段独立集合;
  4. 列表分页;
  5. 使用覆盖常用字段的投影。

3.10 建模原则

  1. 从查询和更新模式出发;
  2. 一起读的数据放一起;
  3. 避免无界数组;
  4. 高频更新计数可单独集合;
  5. 金额、时间、ID 类型要明确;
  6. 常用查询要有索引支撑;
  7. 事务边界尽量小;
  8. 分片可能性要提前评估;
  9. 集合职责清晰,不做万能大杂烩;
  10. 应用契约与 validator 双重约束。

本章小结

文档模型通过嵌套和数组减少不必要的关联,让数据形态接近业务读取方式。但灵活不等于随意,必须控制文档大小、数组增长、字段类型和更新频率。好的 MongoDB 设计通常先列出查询和写入模式,再决定内嵌、引用或混合建模。

思考题

  1. 为什么金额应使用 Decimal128?
  2. 无界数组会带来哪些风险?
  3. 什么时候应该拆分集合?
  4. $lookup 高频出现说明什么?
  5. validator 能替代应用层校验吗?