一个文档在 MongoDB 里到底长什么样?这篇用 pymongo 编码,用不到 50 行的纯 Python 解析器把字节拆开,看总长度、类型字节、键名和值各占多少,再放到真实的 mongod 里用 bsonsize 和 $bsonSize 对账。
0000 0c 00 00 00 10 61 00 01 00 00 00 00 .....a......
前 4 字节是小端的总长度 = 12(= len(bytes) = 12)
第 5 字节 0x10 = 类型 16(int32);键名 b'a' 后跟一个 0 字节;值 01000000 是小端 int32 = 1;最后一个字节 0x00 结束文档
空文档 {} 编码: 0500000000(5 字节 = 4 字节长度 + 1 字节结尾)
纯 Python 解析: [('a', 16, 4, 1)]BSON 文档 = 4 字节小端总长度 + 若干元素 + 1 个 0 结尾。每个元素是 1 字节类型、键名(C 字符串)、值。最小的 {"a": 1} 一共 12 字节。
类型 类型字节 值字节 整个文档
int32 0x10 4 12
int64 0x12 8 16
double 0x01 8 16
string "abc" 0x02 8 16
bool 0x08 1 9
null 0x0a 0 8
ObjectId 0x07 12 20
datetime 0x09 8 16
Decimal128 0x13 16 24
Timestamp 0x11 8 16
binary 4 字节 0x05 9 17
regex /a/i 0x0b 4 12
空数组 0x04 5 13
数组 [1,2] 0x04 19 27
空子文档 0x03 5 13
子文档 {"y":1} 0x03 12 20定长类型:int32 4 字节、int64 / double / datetime / Timestamp 8 字节、ObjectId 12 字节、Decimal128 16 字节、bool 1 字节、null 0 字节。字符串、binary、数组、子文档都是长度在前的变长类型。
"abc" → 字符数 3,UTF-8 字节数 3,编码里的长度前缀 4(= 字节数 + 1),整个文档 16 字节 "" → 字符数 0,UTF-8 字节数 0,编码里的长度前缀 1(= 字节数 + 1),整个文档 13 字节 "中文" → 字符数 2,UTF-8 字节数 6,编码里的长度前缀 7(= 字节数 + 1),整个文档 19 字节 "😀" → 字符数 1,UTF-8 字节数 4,编码里的长度前缀 5(= 字节数 + 1),整个文档 17 字节 键名含 NUL: InvalidDocument: Invalid document: Key names must not contain the NULL byte 字符串值里含 NUL 是允许的: 10000000027300040000006100620000
字符串前面是字节数 + 1 的长度前缀,以 0 结尾;中文每字 3 字节,emoji 4 字节。键名是 C 字符串,不能含 NUL 字节(pymongo 在客户端就报错);字符串值有长度前缀,可以含 NUL。
0000 21 00 00 00 04 61 72 72 00 17 00 00 00 02 30 00 !....arr......0.
0010 02 00 00 00 78 00 02 31 00 02 00 00 00 79 00 00 ....x..1.....y..
0020 00 .
外层: 键 arr,类型 4(array),子文档长度 23;子文档里的键: ['0', '1']
对照 {"0":"x","1":"y"} 这个普通子文档,只有类型字节不同: 210000000461727200170000000230000200000078000231000200000079000000 vs 210000000361727200170000000230000200000078000231000200000079000000;差异位置 [4]数组在 BSON 里是键为 "0"、"1"… 的子文档,和同样内容的普通子文档只有类型字节不同(4 对 3)。
键名 1 字符: 单个文档 26 字节 键名 30 字符: 单个文档 113 字节 集合 short: 20000 个文档,avgObjSize 43 字节,size 860,000,storageSize 421,888 集合 long: 20000 个文档,avgObjSize 130 字节,size 2,600,000,storageSize 544,768
键名在每个文档里都完整存一遍:3 个字段的文档,键名 30 个字符比 1 个字符多 87 字节。集合里的数据总大小差 3 倍,但磁盘上的块被压缩,storageSize 只差约 29%。网络传输和内存里的文档则是原样的大小。
Python 编码长度 86 字节;字段 [('_id', 'int32', 4), ('name', 'string', 11), ('tags', 'array', 23), ('n', 'int64', 8), ('sub', 'document', 10)]
test> bsonsize(db.t.findOne())
86
test> db.t.aggregate([{$project: {_id: 0, size: {$bsonSize: "$$ROOT"}, types: {$map: {input: {$objectToArray: "$$ROOT"}, in: {k: "$$this.k", t: {$type: "$$this.v"}}}}}}])
[
{
size: 86,
types: [
{ k: '_id', t: 'int' },
{ k: 'name', t: 'string' },
{ k: 'tags', t: 'array' },
{ k: 'n', t: 'long' },
{ k: 'sub', t: 'object' }
]
}
]mongosh 的 bsonsize() 和聚合表达式 $bsonSize 给出的字节数与客户端编码出的长度完全一致,都是 86;$type 给出的名字(int、long、object)与类型字节对应。