跳到主要内容

MongoDB ObjectId 结构详解:从 _id 读出文档创建时间

讲清楚 MongoDB ObjectId 的 12 字节结构,前 4 字节时间戳怎么读、随机段和计数器各管什么,怎么从主键直接解出创建时间,以及它和 UUID 的真实差别。

发布于 作者 李雷
#MongoDB #ObjectId #数据库 #后端

MongoDB ObjectId 结构详解:从 _id 读出文档创建时间

很多人用了好几年 MongoDB,却从没认真看过 _id 里那串 24 位十六进制到底是什么。它不是随机生成的一坨乱码,而是一个结构清晰、信息密度很高的值。看懂它,你就能不查 createdAt 字段也知道一条文档什么时候写入,还能直接拿主键跑时间范围查询。

ObjectId 到底由哪几块组成

一个 ObjectId 是 12 字节,显示成 24 个十六进制字符。它的布局是固定的,从左到右分三段:

  • 前 4 字节:32 位、以秒为单位的 Unix 时间戳;
  • 中间 5 字节:每个进程启动时生成一次的随机值;
  • 最后 3 字节:每生成一个 id 就加一的计数器。

记住第一条就够用一大半:前 4 字节就是时间戳。它存的是整秒,不是毫秒,所以这块内嵌时钟的精度是一秒。剩下 5 字节随机段负责区分不同进程和机器,3 字节计数器负责区分同一秒内同一进程产出的多个 id。三段一配合,正常负载下几乎不可能撞车。

怎么从一个 ObjectId 解出创建时间

方法很机械:取前 8 个十六进制字符(也就是前 4 字节),按大端整数读出来,得到的就是 Unix 秒数,再转成日期即可。

举个真实例子。拿到这样一个 ObjectId:

65f1a2b3c4d5e6f708192a3b

拆开看:开头 65f1a2b3 是时间段,c4d5e6f708 是随机段,192a3b 是计数器。把 65f1a2b3 当大端整数算出来是 1710334643,也就是:

2024-03-13 12:57:23 UTC

这条文档就是那一秒写进数据库的。整个过程不需要查任何字段,时钟一直就藏在主键里。你可以直接把 id 粘进 MongoDB ObjectId 生成与解析工具,它会同时给出秒值、UTC 时间和你的本地时区时间;如果手头是一个纯 Unix 秒数想反查日期,可以用 时间戳转换工具 接着算。

生成 ObjectId 时发生了什么

生成不是简单 Math.random()。一个合格的实现会做三件事:把当前 Unix 秒写进前 4 字节、用密码学级随机数填中间 5 字节、让计数器在前一个值基础上加一填最后 3 字节。这样同一批连续生成的 id 既各不相同,又严格按生成顺序升序。

这一点在写测试数据时特别有用。我自己做集成测试时常需要几个看起来像真的、排序又可预期的 _id,批量生成一批塞进种子文件,计数器递增保证它们严格升序,任何「按 _id 排序」的断言在多次运行之间都稳定,不会今天过明天挂。

ObjectId 和 UUID 的真实区别

这是最常被问的问题。一句话:v4 UUID 是 128 位纯随机,里面没有任何可读的时间;ObjectId 是 96 位,前 4 字节是秒级时间戳。

差别落到实处:

  • ObjectId 能告诉你文档何时写入,并且大致按创建顺序排序;随机 UUID 关于时间什么都说不出。
  • ObjectId 更短(12 字节 vs 16 字节),做主键索引更省空间。
  • UUID 有更强的数学唯一性保证,跨系统拼接时不依赖时间或进程。

如果你既想要 UUID 的形态,又想要时间排序,那答案不是 ObjectId,而是 UUIDv7 或 ULID,这两者都内嵌毫秒时间戳。

用 ObjectId 边界跑时间范围查询

既然时间藏在主键里,就能直接拿它切时间段,不必另建 createdAt 索引。做法是为起始秒造最小的 ObjectId、为结束秒造最大的 ObjectId,然后:

{ _id: { $gte: low, $lte: high } }

最小端把随机段和计数器全清零,最大端把它们全拉满。例如从 2024-03-13 12:57:23 起的文档,最小边界就是 65f1a2b30000000000000000。因为 _id 本来就有索引,这个范围扫描很快。

这里有个常见坑:手写边界时忘了后面的字节,只用一个 8 位时间戳后面随便接几个随机字节,结果会悄悄漏掉窗口边缘的文档。最小端必须清零、最大端必须拉满,这一步别省。

几条要记住的事

  • 时间戳是整秒,不是毫秒。相隔 100 毫秒写入的两条文档可能落在同一个 ObjectId 秒里,要在这一秒内排序得靠计数器。
  • 按 _id 排序只在同一进程内严格按时间;同一秒里来自两台服务器的 id 可能交错,严格跨主机排序还得留真正的时间戳字段。
  • 看懂前 4 字节,你就拿回了一块免费的、写在主键里的时钟。

下次再翻到一条没有 createdAt 的文档,别急着加字段,先看一眼它的 _id。


Made by Toolora · Updated 2026-06-13