根本原因是查询时MongoDB需加载整文档而非仅所需字段;应使用投影指定字段、为嵌套路径建索引、按访问模式拆分集合。

大文档导致查询变慢的根本原因
不是文档太大本身慢,而是查询时 MongoDB 不得不把整个文档从磁盘读进内存再过滤字段。比如一个 2MB 的传感器日志文档,你只查 metadata.location,但 WiredTiger 还是得加载全部二进制块(chunks)才能定位那个字段——BSON 的长度前缀虽快,可前提是整块数据已在缓存中。
更麻烦的是:如果该文档频繁更新,WiredTiger 的 MVCC 会保留旧版本,脏页堆积 + 缓存淘汰压力双升,bytes currently in the cache 很快逼近 cachesizegb 上限,触发大量 page eviction。
用投影(projection)强制跳过冗余字段
绝大多数“大文档查询慢”问题,其实根本没配 projection。默认 find({}) 会返回完整文档,哪怕前端只要三个字段。
- 正确写法:
db.sensors.find({status: "active"}, {name: 1, metadata: 1, _id: 0})—— 显式列出需要的字段,_id: 0关闭默认返回 - 对嵌套字段要写全路径:
{ "metadata.location": 1, "readings.0.timestamp": 1 },否则 WiredTiger 无法做字段级裁剪 - 避免
$slice或正则在 projection 中滥用,它们会让引擎放弃字段裁剪,退化为全文档加载
为嵌套路径建索引比为整个数组建索引更有效
很多人给 readings 数组建索引:db.sensors.createIndex({"readings": 1}),这会导致索引体积爆炸(每个数组元素都生成索引项),且无法加速 readings.0.timestamp 这类精确路径查询。
真正该做的是路径粒度索引:
- 查最新一条记录时间:
db.sensors.createIndex({"readings.0.timestamp": -1}) - 查某时间段内所有读数:
db.sensors.createIndex({"readings.timestamp": 1, "readings.value": 1})(复合索引,注意顺序) - 地理查询加速:
db.sensors.createIndex({"metadata.location": "2dsphere"}),配合geoNear可直接走索引,不用加载metadata全体
验证是否生效:用 explain() 看 executionStats.nReturned 是否等于 nscannedObjects,且 cursor 类型是 BtreeCursor 而非 BasicCursor。
当文档实在太大,拆比索引更可靠
单文档超过 16MB(MongoDB 硬限制)当然必须拆;但即使远低于此,比如平均 800KB 的设备快照,也建议在应用层按访问模式切分:
- 热数据(当前状态、最近 1 小时读数)保留在主集合
sensors - 冷数据(历史读数归档)移入
sensors_history,用GridFS存或独立集合 + 时间范围分片 - 元数据(固件、配置)抽成
sensors_meta,用$lookup按需关联,避免每次查询都拖着 BinData 字段
固定集合(capped: true)不适合这种场景——它不支持按条件删除,而大文档归档恰恰需要按时间 TTL 清理。真正该用 capped 的,是纯追加日志类数据,比如操作审计流。


















