文档与 BSON
JSON 风格文档、_id、嵌套与数组。
文档与 BSON
这一课你将能做到
- 说清 BSON 与 JSON 的关系,认出
ObjectId、Date、Decimal128 等常见 BSON 类型
- 设计含嵌套对象与数组的文档,解释
_id 的生成规则与 16 MB 上限的工程含义
- 对「内嵌 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 / Int64 | 42 / NumberLong("42") | 整数;JS 精度问题用 Long |
| Double | 3.14 | 浮点;金额慎用 |
| Decimal128 | NumberDecimal("9.99") | 高精度十进制,金额首选 |
| Boolean | true | |
| Date | ISODate("2026-01-15T08:00:00Z") | UTC 毫秒 |
| ObjectId | ObjectId("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
步骤 3:认识 ObjectId 的构成(面试点)
[ 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。思考两个问题:
- 作者信息为什么存快照而不是只存 userId?(展示时省一次关联查询)
- 为什么评论独立成集合而不是内嵌进 track 文档?(写入频繁、需要分页、会无限增长)
讲清楚要点
- 每个文档必须有
_id 且集合内唯一:未提供时自动生成 ObjectId;自定义 _id 用稳定业务键,可变字段(手机号、昵称)绝不当 _id。
- ObjectId 自带时间信息:4 字节秒级时间戳 + 5 字节随机 + 3 字节计数器,可粗排先后、天然分布式唯一,但不能替代业务时间字段。
- 类型决定查询行为:日期存字符串就做不了范围查询与时区处理;金额用
Decimal128 或整数分,Double 有浮点误差。
- 内嵌换读性能,引用换更新独立:常一起读、一对少 → 内嵌;独立增长、独立更新、多对多 → 引用或独立集合。
- 16 MB 是硬边界:设计时问一句「这个数组五年后多大」,日志、附件、无限增长的评论一律独立集合。
- 同集合可异构,更要团队约定 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 次查询,丢掉文档优势 | 常读小对象内嵌或存快照 |
卡点急救
- 插入报
can't use part of ... to traverse the element
字段名里带了点号 . 或以 $ 开头——BSON 不允许,改字段名(用下划线或驼峰)。
- 查出来的日期是数字或字符串
写入时就没存成 Date 类型。删掉测试数据,用
ISODate("...") 或 new Date() 重新写。
- 不确定文档里到底存了什么类型
mongosh 里
Object.bsonsize(doc) 看总大小;逐字段用 typeof / instanceof ObjectId / instanceof Date 核对,与团队字段契约对齐。
面试常问
-
ObjectId 的构成是什么?
12 字节:4 字节秒级时间戳 + 5 字节随机值(每进程一份)+ 3 字节递增计数器。好处:分布式下无需协调即可唯一、可粗略按时间排序。注意它不是精确时间戳,业务时间要单独存字段。
-
单文档大小限制是多少?为什么?
16 MB。这是设计决策而非技术天花板:防止单文档过大导致内存、传输与索引效率崩坏。超大内容(附件、日志流水)应拆分集合或外置对象存储,文档里只存引用。
-
内嵌 vs 引用怎么选?
三个问题定夺:一起读吗?一起变吗?会无限增长吗?——一起读且一对少选内嵌;独立生命周期或无限增长选引用/独立集合。本站例:课时摘要可内嵌进 track,评论必须独立集合。
自测三问
- 未指定
_id 时会发生什么?ObjectId 能当创建时间用吗?
- 什么信号出现时,应该把数组从主文档拆成独立集合?
- BSON 相比纯 JSON 文本多了什么能力?
小结一句话
文档是带类型的 BSON 对象:_id 唯一且不可变,嵌套与数组是建模优势,16 MB 与更新模式是设计边界。
延伸