把 BSON 文档逐字节拆开:总长度、类型字节、键名 C 字符串、各类型值的字节数、字符串长度前缀、数组就是键为 "0""1" 的子文档;键名每个文档都存一遍,在内存里差 3 倍、压缩后的磁盘上只差约 29%;用 mongosh 的 bsonsize 与 $bsonSize 验证 Python 编码的字节数。
实测 BSON 跨类型的比较顺序:升序排序依次是 MinKey、null、数字、字符串、对象、BinData、ObjectId、布尔、日期、Timestamp、正则、MaxKey;比较运算只在同一类型括号内匹配,null 与字段缺失的差别,数组按最小或最大元素排序,子文档按字段逐个比较。
实测数字类型混用:int32、int64、double、Decimal128 的 1 在查询里互相相等;int64 与 double 精确比较;0.1 的 double 与 Decimal128 不相等;唯一索引把四种 1 当成同一个值;$add 与 $sum 的类型提升;mongosh 里超过 2^53 的 JS 数字会悄悄变成 double。
实测 ObjectId 的 12 个字节:4 字节秒级时间戳、5 字节进程内随机值、3 字节起点随机的计数器(会回绕);同一进程内严格递增,多个进程之间 _id 的排序不等于创建顺序;_id 缺省时驱动会在发送前补上,绕过驱动服务端也会补。
实测 MongoDB 的文档大小与嵌套深度上限:maxBsonObjectSize 是 16,777,216 字节,刚好等于的文档能插入、多 1 字节被服务端用错误码 10334 拒绝;update 撑过上限的报错;嵌套深度在 9.0.2 上限是 180 层;聚合里 $push 出的文档可以超过 16 MiB 返回客户端,但写进集合会失败。
实测 BSON 字段顺序:子文档整体匹配、$expr 比较、$group 分组键和唯一索引都把字段顺序当成值的一部分,点号路径匹配与顺序无关;$set 保持原位、新字段追加末尾,$unset 再 $set 跑到末尾;_id 总在最前;mongosh 的 JS 对象会把整数键排到最前面。
实测 mongod 的 dbpath 目录:WiredTiger、turtle、_mdb_catalog.wt、sizeStorer.wt、storage.bson、mongod.lock 各是什么;每个集合、每个索引一个 .wt 文件,文件名里的 ident 用 collStats 与 $listCatalog 对应;journal 是 100 MiB 的稀疏预分配文件;drop 集合后对应文件消失。
实测 WiredTiger 的块压缩:同样 100,000 个日志样文档,none、snappy、zstd、zlib 的 storageSize 是 BSON 总大小的 104%、46%、31%、35%;插入耗时差别不大,冷缓存扫描 zstd 与 zlib 约是 snappy 的两倍;随机数据几乎不可压缩;集合默认 snappy,索引用前缀压缩。
实测删数据后磁盘为什么不还:删掉 90% 的文档后 dataSize 立刻下降,磁盘文件反而略增,空间变成“可复用空闲”留在文件里;再插入会复用它;compact 把 36 MB 空闲空间还给操作系统(文件 45.5 MB → 9.2 MB);compact 期间读写照常;drop 集合则立即删掉文件。
实测 kill -9 之后哪些写入还在:w:1 下每次丢 192~420 条已确认的写入,j:true 零丢失但吞吐降到约 1/35;journalCommitInterval 越大丢得越多(中位数 96、198、306 条);没有 checkpoint 时恢复靠 journal 重放;末尾补一次 j:true 能把之前的写入一起落盘。
实测把 WiredTiger 缓存压到最小的 256 MB:装入两倍于缓存的数据时缓存占用不超过上限;工作集大于缓存时每次读平均要读入 0.62 个页;8 线程写入压力下脏数据封顶在缓存的 10%,应用线程被迫淘汰的耗时从 2 ms 增到 514 ms,吞吐只低约 7%;serverStatus 里直接给出淘汰阈值。
实测 capped 集合:max 限制文档数、size 限制字节数,写满后最老的先被覆盖,自然顺序即插入顺序;9.0.2 上删除单个文档和把文档改大都能成功(与旧说法不同);tailable 游标实时收到新文档;convertToCapped 与 collMod 可以在线转换和改大小。