先记住这个答案
因为MongoDB文档有16MB BSON大小限制,数组无界增长最终会撑爆文档;同时多键索引会因数组元素增多而膨胀,写放大和内存占用上升;更新时需要重写整个文档,匹配大数组的查询也会变慢。应该用上限数组或拆分为子集合。
- 无界数组迟早触发16MB文档限制
- 数组增删导致索引膨胀和写放大
- 用上限数组或拆分为引用集合
文档膨胀与索引膨胀的机制
MongoDB单文档最大BSON大小为16MB。当数组字段不断追加元素,文档体积线性增长,最终写入会报错。即便未达上限,每次插入元素也要更新整个文档,存储引擎重写文档,写放大明显,尤其当文档被移动时更慢。
对数组建索引会创建多键索引,每个数组元素对应一个索引项。无界数组使索引条目无限增加,索引体积膨胀,内存占用和磁盘I/O上升。同时更新数组需要同步维护多个索引条目,写入成本随数组长度增加。查询匹配大数组时,扫描的索引条目多,性能退化。
博客系统的评论数组案例
假设一个blog文档嵌入comments数组,每个评论有author和text。热门文章评论数增长快,一年内可达10万条。每条评论平均500字节,总评论约50MB,远超16MB限制。即使控制单条长度,评论也会持续累积,文档无法扩容。
进一步,对comments.author建索引,每个评论都有索引条目,索引体积膨胀,写时维护开销大。查询某篇文章的所有评论需要读取整个大文档,既慢又占内存。实际处理是拆分成comments集合,每个评论引用blog_id,用索引定位,或设评论上限只保留最近100条。
何时数组仍可接受
如果数组有明确业务上限且上限较小时,嵌入数组合理。例如订单的items数量限制为50,或用户标签上限20个。这种有界数组不会无限增长,文档大小可预估,索引和查询开销可控。关键在于确认业务是否保证数组长度永远不超阈值。
若无法用业务强制上限,就应选择引用集合,或使用 $push 配合 $slice 只保留最近 N 条。添加约束需要应用层每次写入时校验,或用 $push 搭配 $slice 原子截断;但 MongoDB 没有声明式的数组最大长度限制,$slice 会丢弃旧数据而非拒绝新增。代价是需要额外查询或丢弃数据,但换来文档稳定和查询性能可预期。
容易答错的地方
- 觉得可以用$push一直加
- 错误认识:MongoDB能存大文档,数组随便加。实际上16MB硬限制,且文档移动频繁,插入到接近上限会极慢,最终报错。必须设计上限或拆分。
- 只担心容量,不担心查询
- 错误认识:只要不到16MB就没事。但数组长了索引膨胀,查询匹配和更新都要处理更多元素,性能迅速下降。即使容量允许,也应避免无界。
面试官还会怎么问?
可以用Capped集合限制数组长度吗?
Capped集合限制的是集合大小,不是文档内部数组长度。要限制数组长度需要在应用层原子地更新,例如用$push配合$slice,但$slice只保留最后N个,不是持久约束。真正可靠的是业务强校验或拆分。
多键索引对每个元素都建条目,更新成本具体多高?
每次向数组添加一个元素,除了更新文档外,还要为这个新值插入一个索引条目。索引条目总数随数组长度增长,导致索引体积增大、内存占用增加,缓存命中率下降,写入吞吐下降。但每次插入的索引维护成本是 O(log N),并非与 N 线性相关。
无界数组是否一定导致16MB?有没有逃逸方法?
理论上,只要持续添加元素就会逼近 16MB,最终可能触发限制。压缩不能作为规避手段,因为 MongoDB 没有针对单个文档的压缩功能。更合理的做法是设置业务上限或拆分为子集合。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。