bigywater.tech
MongoDB · 9

BSON 编码:一个文档在字节层面长什么样

一个文档在 MongoDB 里到底长什么样?这篇用 pymongo 编码,用不到 50 行的纯 Python 解析器把字节拆开,看总长度、类型字节、键名和值各占多少,再放到真实的 mongod 里用 bsonsize 和 $bsonSize 对账。

12 字节
{"a": 1} 的 BSON 编码长度;空文档 {} 是 5 字节
26 → 113
同样 3 个整数字段,键名从 1 个字符变成 30 个字符,单个文档的字节数
86
服务端的 bsonsize 与 $bsonSize 返回的字节数,和 Python 编码出的长度一致

文档的结构

实测 · 本机执行输出
  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)与类型字节对应。

分步演示

BSON 编码
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。纯 Python 解析器只实现了本文用到的类型;字节来自 pymongo 的 bson.encode,服务端对账用 mongosh
    • 没有验证:压缩算法对 storageSize 的具体影响(只比较了默认 snappy 下的两个集合);非 UTF-8 的字符串怎么处理;BSON 的 deprecated 类型(undefined、DBPointer、symbol 等);JavaScript 代码类型;binary 各子类型(UUID 等)的差别;最大嵌套深度与 16MB 上限(见后面的文章);big-endian 平台
    京ICP备2026067502号-1京公网安备11010502063487号