CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
MongoDB 入门/课时/文档与 BSON
RawBlame
课时目录12 节

MongoDB 入门

  1. 01MongoDB 是什么
  2. 02安装与 mongosh
  3. 03文档与 BSON
  4. 04CRUD 操作
  5. 05查询与投影
  6. 06索引
  7. 07聚合管道
  8. 08Spring Data MongoDB
  9. 09何时用 Mongo vs SQL
  10. 10Docker 运行 MongoDB
  11. 11Node.js 驱动与 Mongoose
  12. 12综合练习

文档与 BSON

JSON 风格文档、_id、嵌套与数组。

30 分钟

文档与 BSON

这一课你将能做到

  1. 说清 BSON 与 JSON 的关系,认出 ObjectId、Date、Decimal128 等常见 BSON 类型
  2. 设计含嵌套对象与数组的文档,解释 _id 的生成规则与 16 MB 上限的工程含义
  3. 对「内嵌 vs 引用」给出结合本站场景(课时列表、评论)的可辩护选择

为什么要学

CRUD 语法半小时就能背下来,真正决定项目成败的是数据长什么样。把每次都一起读的数据拆成十次查询,或把无限增长的列表硬塞进一个文档,都会在数据量上来后爆发。

本课建立文档思维:字段类型、主键规则、内嵌边界。这是 MongoDB 面试的必考区——ObjectId 怎么构成、单文档多大、内嵌还是引用,几乎每场涉及 Mongo 的面试都会问。

人话:文档 = 一份带类型的「超级 JSON」;BSON(Binary JSON)是数据库里真正存的二进制格式,多了类型和长度信息。

先建立图像

应用里的对象(Java / JavaScript)
      ↓ 驱动序列化
BSON Document(≤ 16 MB)
  ├─ _id:ObjectId / 自定义唯一值
  ├─ 标量字段:String / Int32 / Double / Date / Boolean …
  ├─ 嵌套文档:{ "a": { "b": 1 } }
  └─ 数组:[1, "a", { "x": 2 }]
      ↓ 存储
某个 Collection 里(同集合文档可异构)

常用 BSON 类型速查:

类型mongosh 示例说明
String"hello"UTF-8
Int32 / Int6442 / NumberLong("42")整数;JS 精度问题用 Long
Double3.14浮点;金额慎用
Decimal128NumberDecimal("9.99")高精度十进制,金额首选
Booleantrue
DateISODate("2026-01-15T08:00:00Z")UTC 毫秒
ObjectIdObjectId("66f1a2…")默认主键
Array / Embedded[1, "a", {}] / { "a": 1 }有序数组 / 嵌套文档

内嵌与引用的判断口诀:

内嵌:读一次拿齐 → 一对少、一起读、一起消失(如订单 + 订单项)
引用:存对方 _id,另查一次 → 一对多、独立生命周期(如课程 + 评论)

跟着做

步骤 1:插入一份「课程」风格文档

use coderoadmap_lab

db.tracks.insertOne({
  slug: "mongodb-essentials",
  title: "MongoDB 入门",
  meta: {
    level: "beginner",     // 嵌套对象:元数据
    weeks: 2
  },
  lessons: [               // 对象数组:课时摘要
    { slug: "mongo-intro", order: 1 },
    { slug: "mongo-install", order: 2 }
  ],
  createdAt: ISODate("2026-01-15T08:00:00Z")
})

应看到:返回 { acknowledged: true, insertedId: ObjectId("…") }——你没写 _id,服务器自动生成了。

步骤 2:观察自动 _id 与类型

db.tracks.findOne({ slug: "mongodb-essentials" })

// 看某字段类型
typeof db.tracks.findOne().createdAt        // object(mongosh 中 Date 是对象)
db.tracks.findOne()._id instanceof ObjectId // true

记下 _id 的字符串形式,后面做引用实验要用。

步骤 3:认识 ObjectId 的构成(面试点)

ObjectId 共 12 字节:

[ 4 字节时间戳 ][ 5 字节随机值 ][ 3 字节递增计数器 ]
   秒级创建时间     进程级唯一       同进程内自增

含义:_id 自带创建时间序(可粗略按 _id 排新旧);随机值保证多机生成不冲突;计数器保证同进程并发不重复。但它不是精确业务时间——秒级粒度、且非单调严格有序,业务时间请单独存 createdAt。

步骤 4:自定义 _id 与重复冲突

// 业务主键当 _id:适合全局唯一且几乎不变的业务键
db.tracks.insertOne({
  _id: "git-essentials",
  title: "Git 版本控制入门",
  meta: { level: "beginner", weeks: 1 },
  lessons: []
})

db.tracks.findOne({ _id: "git-essentials" })

// 再插一次同 _id → E11000 duplicate key error
db.tracks.insertOne({ _id: "git-essentials", title: "重复" })

应看到:第二次插入报重复键错误——_id 唯一由集合级约束保证。

步骤 5:量一下文档大小

const doc = db.tracks.findOne({ slug: "mongodb-essentials" })
Object.bsonsize(doc)   // 返回字节数,远小于 16 MB

笔记写:单文档硬上限 16 MB。推论:课时列表若含上万节正文、评论流、传感器流水,都不该无限 $push 进一个文档。

步骤 6:纸上设计 comments 集合

为「课程评论」设计文档字段:_id、trackSlug、body、author: { userId, displayName }(快照)、mentions: []、createdAt。思考两个问题:

  1. 作者信息为什么存快照而不是只存 userId?(展示时省一次关联查询)
  2. 为什么评论独立成集合而不是内嵌进 track 文档?(写入频繁、需要分页、会无限增长)

综合练习课会把这份设计落地。

讲清楚要点

  1. 每个文档必须有 _id 且集合内唯一:未提供时自动生成 ObjectId;自定义 _id 用稳定业务键,可变字段(手机号、昵称)绝不当 _id。
  2. ObjectId 自带时间信息:4 字节秒级时间戳 + 5 字节随机 + 3 字节计数器,可粗排先后、天然分布式唯一,但不能替代业务时间字段。
  3. 类型决定查询行为:日期存字符串就做不了范围查询与时区处理;金额用 Decimal128 或整数分,Double 有浮点误差。
  4. 内嵌换读性能,引用换更新独立:常一起读、一对少 → 内嵌;独立增长、独立更新、多对多 → 引用或独立集合。
  5. 16 MB 是硬边界:设计时问一句「这个数组五年后多大」,日志、附件、无限增长的评论一律独立集合。
  6. 同集合可异构,更要团队约定 schema:字段增减要过评审,否则索引与统计迟早失控。

你可能会懵的地方

  • JSON 和 BSON 到底差在哪? JSON 是文本格式,类型少(无日期、无整数浮点之分);BSON 是二进制格式,带类型与长度信息,支持 ObjectId、Date、Decimal128、二进制等。mongosh 打印时「看起来像 JSON」,但传输和存储是 BSON。

  • _id 能改吗? 不可变。要换主键只能删旧插新。所以 _id 必须选稳定值,业务上可变的字段一律做普通字段。

  • 数组里嵌对象,之后怎么查询? 点号路径(如 lessons.slug)或 $elemMatch。设计上要避免「数组套数组套对象」这种难以索引的结构。

  • 内嵌深度有官方建议吗? 官方建议避免超过约 100 层嵌套(远超实际需要);工程上真正要控制的不是层数,而是「大数组 + 频繁更新」的组合——它带来文档迁移与写冲突。

  • 和 MySQL JSON 列的本质区别? MySQL JSON 列在关系模型内提供半结构化能力,索引与查询受限;MongoDB 整个存储引擎、索引体系(多键索引)、聚合管道都按文档模型设计。

错法对照

错法问题正解
_id 用手机号等可变字段业务一变主键就废_id 用稳定值;手机号做普通字段
无限 $push 评论进 track 文档逼近 16 MB、写冲突、难分页独立 comments 集合
日期存字符串 "2026-01-15"无法范围查询、无法排序语义正确用 ISODate / Date 类型
金额存 Double浮点误差累积NumberDecimal / 整数分
所有关联都拆出去靠引用读一次要 N 次查询,丢掉文档优势常读小对象内嵌或存快照

卡点急救

  1. 插入报 can't use part of ... to traverse the element 字段名里带了点号 . 或以 $ 开头——BSON 不允许,改字段名(用下划线或驼峰)。
  2. 查出来的日期是数字或字符串 写入时就没存成 Date 类型。删掉测试数据,用 ISODate("...") 或 new Date() 重新写。
  3. 不确定文档里到底存了什么类型 mongosh 里 Object.bsonsize(doc) 看总大小;逐字段用 typeof / instanceof ObjectId / instanceof Date 核对,与团队字段契约对齐。

面试常问

  • ObjectId 的构成是什么? 12 字节:4 字节秒级时间戳 + 5 字节随机值(每进程一份)+ 3 字节递增计数器。好处:分布式下无需协调即可唯一、可粗略按时间排序。注意它不是精确时间戳,业务时间要单独存字段。

  • 单文档大小限制是多少?为什么? 16 MB。这是设计决策而非技术天花板:防止单文档过大导致内存、传输与索引效率崩坏。超大内容(附件、日志流水)应拆分集合或外置对象存储,文档里只存引用。

  • 内嵌 vs 引用怎么选? 三个问题定夺:一起读吗?一起变吗?会无限增长吗?——一起读且一对少选内嵌;独立生命周期或无限增长选引用/独立集合。本站例:课时摘要可内嵌进 track,评论必须独立集合。

自测三问

  1. 未指定 _id 时会发生什么?ObjectId 能当创建时间用吗?
  2. 什么信号出现时,应该把数组从主文档拆成独立集合?
  3. BSON 相比纯 JSON 文本多了什么能力?

小结一句话

文档是带类型的 BSON 对象:_id 唯一且不可变,嵌套与数组是建模优势,16 MB 与更新模式是设计边界。

延伸

  • Documents 官方文档
  • Data Model Design 建模指南
  • 下一课:CRUD 操作——把文档增删改查跑通。
加载本节练习…
← 上一课安装与 mongosh下一课 →CRUD 操作