内嵌和引用

MongoDB 建模时,经常要决定:

相关数据是放在同一个文档里,还是拆到不同集合里?

这就是内嵌和引用的选择。

一、内嵌

内嵌就是把相关数据直接放在当前文档里。

db.posts.insertOne({
  title: "MongoDB 入门",
  author: {
    id: ObjectId("665f0e0b6f8c2b5f8e123456"),
    username: "zhangsan"
  },
  tags: ["mongodb", "database"]
})

优点:

  • 查询一次就能拿到完整数据。
  • 结构接近接口响应。
  • 不需要额外关联查询。

缺点:

  • 内嵌数据重复时,更新会麻烦。
  • 文档可能变大。
  • 数组无限增长会影响性能和维护。

二、引用

引用就是只保存另一个文档的 _id

db.posts.insertOne({
  title: "MongoDB 入门",
  authorId: ObjectId("665f0e0b6f8c2b5f8e123456")
})

用户单独存:

db.users.insertOne({
  _id: ObjectId("665f0e0b6f8c2b5f8e123456"),
  username: "zhangsan"
})

优点:

  • 数据不容易重复。
  • 单独更新用户信息更方便。
  • 适合一对多、多对多关系。

缺点:

  • 查询时可能要查多次。
  • 或者使用 $lookup 做聚合关联。

三、什么时候内嵌

适合内嵌:

  • 数据经常一起读取。
  • 子数据数量有限。
  • 子数据不需要单独频繁查询。
  • 子数据生命周期依赖父文档。

例如文章的少量标签:

{
  title: "MongoDB 入门",
  tags: ["mongodb", "database"]
}

例如用户资料:

{
  username: "zhangsan",
  profile: {
    city: "杭州",
    bio: "后端开发"
  }
}

四、什么时候引用

适合引用:

  • 子数据数量可能很多。
  • 子数据需要单独查询、分页、排序。
  • 子数据会被多个文档共享。
  • 子数据经常单独更新。

例如文章评论:

db.comments.insertOne({
  postId: ObjectId("665f0e0b6f8c2b5f8e123456"),
  userId: ObjectId("665f0e0b6f8c2b5f8e654321"),
  content: "写得不错",
  createdAt: new Date()
})

评论数量可能非常多,通常不建议全部内嵌到文章文档里。

五、不要照搬 MySQL 的设计

MongoDB 不是简单把 MySQL 表换成集合。

MySQL 常见做法:

users
posts
comments
tags
post_tags

MongoDB 可以根据读取场景调整:

{
  title: "MongoDB 入门",
  author: {
    id: ObjectId("..."),
    username: "zhangsan"
  },
  tags: ["mongodb", "database"]
}

也就是说,MongoDB 建模更关注“接口怎么读、数据怎么变”。

六、简单判断口诀

问题倾向
是否总是一起读?内嵌
数量是否有限?内嵌
是否需要单独分页?引用
是否会被多个地方共享?引用
是否经常单独更新?引用

新手阶段不用追求一次设计完美,但要避免把无限增长的数据塞进一个文档。