ObjectId 的 12 个字节各是什么?是不是严格递增?这篇把它拆成时间戳、随机值、计数器三段,在多个进程里实测它的顺序,并确认 _id 缺省时到底是驱动还是服务端生成的。
十六进制 时间戳(4) 随机(5) 计数器(3) 6acc348d25e7f1e741141771 6acc348d 25e7f1e741 141771 = 1316721 6acc348d25e7f1e741141772 6acc348d 25e7f1e741 141772 = 1316722 6acc348d25e7f1e741141773 6acc348d 25e7f1e741 141773 = 1316723 6acc348d25e7f1e741141774 6acc348d 25e7f1e741 141774 = 1316724 时间戳 1791767693 秒 = 2026-10-12 01:14:53 UTC(pymongo 的 generation_time)
前 4 字节是秒级时间戳(大端),中间 5 字节是进程启动时生成的随机值,后 3 字节是计数器(起点随机,每次 +1)。所以同一进程内一连串 ObjectId 只有计数器在变。
连续生成 100,000 个:覆盖的秒数偏移 [0](相对生成前的整秒);其中第 1 个和最后 1 个的计数器 1316725 → 1416724 整批在同一个进程里 5 字节随机部分的取值个数: 1 ObjectId.from_datetime(2026-01-01) = 6955b9000000000000000000(后面 8 字节全 0,常用来做按时间范围的 _id 查询边界)
同一秒生成的 ObjectId 时间部分相同,靠计数器区分;ObjectId.from_datetime 把后 8 字节置 0,可以当按时间范围查询 _id 的边界。
进程 1: 6acc348d 20109c624f 725910 6acc348d 20109c624f 725911 进程 2: 6acc348d 20dfcae807 96f286 6acc348d 20dfcae807 96f287 进程 3: 6acc348d 1e3839d63a c66a7e 6acc348d 1e3839d63a c66a7f
每个进程的随机 5 字节和计数器起点都不同,所以多个进程同一秒内生成的 ObjectId 互不冲突,但谁先谁后由随机值而不是时间决定。
6acc348d25e7f1e741fffffd 计数器 fffffd 6acc348d25e7f1e741fffffe 计数器 fffffe 6acc348d25e7f1e741ffffff 计数器 ffffff 6acc348d25e7f1e741000000 计数器 000000 6acc348d25e7f1e741000001 计数器 000001
3 字节计数器到 ffffff 后回绕到 0,这时同一秒内生成的 ObjectId 会比前一个小。
3 个进程各生成约 2348 个,共 7044 个 ObjectId;按创建时间(纳秒时钟)排好之后,相邻两个中 _id 反而变小的有 3394 对 按 _id 排序是否等于按创建时间排序: False 进程 1 内部是否严格递增: True 进程 2 内部是否严格递增: True 进程 3 内部是否严格递增: True
单个进程内严格递增(不回绕时);多个进程并发时,按 _id 排序和按创建时间排序是两回事。不要把 _id 当严格的创建顺序。
test> oid = ObjectId("65a1b2c3d4e5f60718293a4b"); [oid.getTimestamp(), oid.toString(), oid.id.toString("hex").length]
[ ISODate('2024-01-12T21:44:35.000Z'), '65a1b2c3d4e5f60718293a4b', 24 ]
pymongo: insert_one 之前 doc = {'y': 2};
insert_one 之后同一个字典变成 {"y": 2, "_id": ObjectId(...)}: ['_id', 'y'],_id 的类型 ObjectId,与 inserted_id 相同: True
用原始 insert 命令发送一个没有 _id 的文档 {"z": 1},读回来: ['_id', 'z'],_id 类型 ObjectId
test> db.c3.aggregate([{$project: {t: {$toDate: "$_id"}, idtype: {$type: "$_id"}}}])
[
{
_id: ObjectId('6acc349052406f65c30f4027'),
t: ISODate('2026-10-12T01:14:56.000Z'),
idtype: 'objectId'
}
]驱动在发送前给文档补上 _id(pymongo 会修改你传入的字典);如果文档到了服务端还没有 _id,服务端也会生成一个 ObjectId。