嵌入适用于读多写少且子文档生命周期与父文档一致的场景,否则应引用;数组需分桶或截断防失控;索引须严格匹配查询字段顺序与类型;文档大小需精确估算避免临界性能问题。

读写比例决定嵌入还是引用
读多写少(比如80%读/20%写)的场景下,嵌入文档能显著降低查询延迟——实测从23ms降到5ms,因为一次find()就能拿到完整业务对象。但反过来,如果写操作频繁且涉及子数据更新(如IoT设备每秒上报),嵌入会导致整个文档重写,触发磁盘迁移,吞吐量反而从3500 ops/s掉到1200 ops/s。
关键判断点:
- 子文档是否随父文档生命周期一致?一致才适合嵌入
- 更新是否总需要改整个数组?如果是,
$push或$addToSet比全量替换更安全 - 是否存在“只查某条子记录”的需求?有就该拆成独立集合
数组增长失控会拖垮性能
把评论、日志、订单项直接塞进数组看似简单,但一旦数组长度突破千级,文档就会面临三重压力:BSON序列化变慢、内存映射页碎片化、更新时容易超出16MB限制。MongoDB官方明确警告:数组不应无限增长。
替代方案不是“不用数组”,而是控制增长维度:
- 用
bucket_date字段分桶,按天/小时聚合遥测数据 - 对评论启用
$slice投影,只返回最新10条 - 当数组项超200条时,自动切出新文档并用
next_bucket_id链接
复合索引不匹配查询模式等于没建
很多人建了{ status: 1, last_login: 1 }索引,却执行db.users.find({ last_login: { $gt: ... } }).sort({ created_at: -1 })——这个查询根本用不上索引,因为sort字段不在索引中,且status没出现在查询条件里。
真正有效的索引必须严格对齐查询链:
- 等值查询字段放最前(如
status: "active") - 范围查询字段紧随其后(如
last_login: { $gt: ... }) - 排序字段必须是索引最后一项,且方向一致(
created_at: -1对应索引created_at: -1)
漏掉任意一环,explain()里都会显示stage: COLLSCAN。
文档大小超限不是报错才踩坑
16MB限制是硬边界,但实际问题往往在逼近临界值时就已发生:网络传输耗时翻倍、WiredTiger缓存命中率骤降、updateOne()操作可能因文档重定位失败。
估算文档大小不能只看JSON字面量:
- BSON额外开销约10字节/字段 + 字段名长度 + 值实际字节数
- 字符串值按UTF-8编码字节数计算,不是字符数
- 数组里每个元素都单独计费,
comments里100条记录比1条多占近百倍空间
最容易被忽略的是:时间戳用Date类型只占8字节,但存成字符串会膨胀到24字节以上。











