MongoDB 的 ObjectId 和 BSON 有什么特点?
简化版
MongoDB 文档底层用 BSON 存储,BSON 是 JSON 的二进制扩展,支持 Date、ObjectId、二进制、不同数字类型等。ObjectId 通常作为 _id 默认值,它包含时间戳等信息,基本能保证分布式生成时的唯一性,但不是严格连续递增 ID。
详细版
ObjectId 是 MongoDB 最常见的主键类型,长度为 12 字节,通常展示成 24 位十六进制字符串。它的前 4 字节与生成时间有关,所以大体按时间递增,但不能把它当成业务序号或绝对时间排序依据。
BSON 是 MongoDB 的二进制文档格式,相比普通 JSON,它能保留更多类型信息,例如日期、长整型、Decimal128、二进制数据和 ObjectId。面试回答时要强调:MongoDB 看起来像存 JSON,实际存的是 BSON;_id 必须唯一,可以用默认 ObjectId,也可以由业务生成,但不要随意把业务含义塞进 _id。
完整版教学
一、BSON 解决的是 JSON 类型不足的问题
很多人说 MongoDB 存 JSON,这句话方便理解,但不够准确。MongoDB 文档实际以 BSON 形式存储,BSON 可以理解为 Binary JSON。
普通 JSON 类型很少,主要是字符串、数字、布尔、数组、对象和 null。数据库系统还需要表达更多类型,比如日期、二进制、精确小数、不同位宽的整数,以及专门的对象标识。
| 类型 | JSON 是否天然支持 | BSON 常见表达 | 面试关注点 |
|---|---|---|---|
| 日期 | 否 | Date | 避免把时间都当字符串 |
| 主键 | 否 | ObjectId | 默认 _id 常用类型 |
| 精确小数 | 否 | Decimal128 | 金额不要随便用浮点 |
| 二进制 | 否 | BinData | 图片文件通常不直接塞大文档 |
记忆钩子:应用看见的是类 JSON 文档,MongoDB 落盘和网络传输里的核心格式是 BSON。
二、ObjectId 是默认主键,不是业务编号
MongoDB 每个文档都必须有 _id 字段,并且在一个集合内唯一。如果插入文档时没有提供 _id,驱动通常会自动生成 ObjectId。
ObjectId 常见展示形式如下:
{
_id: ObjectId("65f1a2b3c4d5e6f789012345"),
name: "Alice",
createdAt: ISODate("2026-07-29T10:00:00Z")
}
它很适合做技术主键,因为生成成本低,不依赖中心化发号器,也能在多个客户端并发生成时保持极低冲突概率。
但它不适合做订单号、用户编号、流水号。业务编号通常要求可读、可校验、可按规则生成,ObjectId 不承担这些业务语义。
三、ObjectId 的 12 字节结构为什么常被追问
ObjectId 长度是 12 字节,通常编码成 24 个十六进制字符。可以按下面方式记:
ObjectId 12 bytes
4 bytes timestamp
+ 5 bytes process/random
+ 3 bytes counter
= 12 bytes
前 4 字节是秒级时间戳,所以 ObjectId 大体随时间增长。后面的随机或进程相关部分加计数器,用来降低不同机器、不同进程、同一秒内并发生成的冲突概率。
这也是为什么很多调试工具能从 ObjectId 里解析出生成时间。但注意它只有秒级时间,并且受客户端生成时间影响,不能替代严谨的 createdAt 字段。
四、_id 索引是天然存在的唯一索引
MongoDB 会为 _id 建唯一索引。查询单条文档时,按 _id 查通常是最高效路径之一。
db.users.findOne({ _id: ObjectId("65f1a2b3c4d5e6f789012345") })
如果把 _id 设计成很大的字符串或复杂对象,会直接影响索引体积、内存占用和写入成本。面试里可以补一句:_id 不只是字段,它还会进入唯一索引,所以主键类型会影响存储和性能。
常见工程选择:
| 主键方案 | 优点 | 风险 |
|---|---|---|
| 默认 ObjectId | 简单、分布式友好 | 不适合业务可读编号 |
| 业务字符串 | 可读、可对账 | 索引更大,规则变更麻烦 |
| 雪花 ID | 趋势递增、跨库统一 | 依赖发号算法和时钟处理 |
五、ObjectId 大体有序,但别夸大有序性
因为 ObjectId 前 4 字节是时间戳,同一集合中按 _id 排序,通常能得到大体按插入时间排列的结果。
db.logs.find().sort({ _id: -1 }).limit(20)
这个写法在简单日志列表里有时可用,但它不是严谨的业务排序。原因有 3 个:
- ObjectId 可以由客户端生成,客户端时间可能不准。
- 秒级时间戳不能区分同一秒内大量写入的真实先后。
- 导入历史数据或手动指定
_id会破坏默认假设。
严谨做法是显式维护 createdAt,并配合 _id 做稳定排序:
db.logs.find().sort({ createdAt: -1, _id: -1 }).limit(20)
六、BSON 文档也有大小边界
MongoDB 单个 BSON 文档有大小限制,常见上限是 16MB。这个限制会影响建模:不能把无限增长的评论、日志、消息明细都一直嵌进一个文档。
如果一个用户文档里维护 orders: [],用户订单不断增加,文档会越来越大,更新数组也越来越重。更好的方式通常是把订单放到 orders 集合,用 userId 关联。
// 容易膨胀
{ _id: 1, orders: [ /* 可能无限增长 */ ] }
// 更可控
{ _id: 1001, userId: 1, amount: 99, createdAt: ISODate() }
面试里提到 BSON 时,不要只说“二进制 JSON”,还要能联系到类型表达、文档大小、索引大小和建模边界。
七、常见误区与追问
- 误区:MongoDB 存的就是普通 JSON。 更准确说法是文档模型像 JSON,实际存储格式是 BSON,支持更多数据库类型。
- 误区:ObjectId 是全局严格递增 ID。 它大体带时间趋势,但不是严格全局序列,也不适合当业务流水号。
- 误区:有 ObjectId 就不需要 createdAt。 ObjectId 只能粗略反映生成时间,严谨查询、排序和审计仍建议单独存时间字段。
- 追问:为什么
_id类型会影响性能?_id有唯一索引,字段越大,索引占用和比较成本越高。 - 追问:ObjectId 能不能由业务自己生成? 可以,只要保证集合内唯一,但要权衡生成规则、索引体积和业务耦合。
- 追问:BSON 的 16MB 限制会影响什么设计? 会影响无限增长数组、大文档、内嵌历史明细等建模方式。
八、加强记忆
记 MongoDB 的 ObjectId 和 BSON,可以串成“BSON 管类型,ObjectId 管默认身份”。BSON 让文档不止是普通 JSON,能表达日期、二进制、Decimal128 和 ObjectId;ObjectId 是 _id 的常见默认值,12 字节、24 位十六进制展示、前 4 字节带秒级时间,所以大体按时间增长。面试中要主动补边界:ObjectId 不是业务编号,不保证严格连续,不能替代 createdAt;BSON 文档也有大小限制,大数组和大对象要重新考虑集合拆分。