必须用复合索引覆盖非数组字段并严格投影,因MongoDB加载文档时会完整解析超大数组,导致内存与CPU压力陡增;数组字段无法参与索引覆盖,需将status、updated_at等常用字段建复合索引,并配合$slice控制返回长度,再通过explain验证totalDocsExamined为0。

为什么对大数组字段查得慢?
当文档里有个几十KB的 logs 或 history 数组,每次 find({status: "done"}) 都会把整份文档加载进内存——哪怕你只关心 status 和 updated_at。MongoDB 默认不跳过未投影字段,尤其数组字段会触发完整 BSON 解析,CPU 和内存压力陡增。
用复合索引覆盖数组外的关键字段
数组字段本身不能用于覆盖查询(MongoDB 不支持数组内容直接进索引覆盖),但你可以把**数组以外的常用过滤/返回字段**全塞进一个复合索引里,并在查询时严格投影。
- 错误做法:
db.orders.createIndex({items: 1, status: 1})——items是数组,这个索引无法支撑覆盖查询 - 正确做法:
db.orders.createIndex({status: 1, updated_at: -1, user_id: 1, _id: 1}),然后查:db.orders.find({status: "shipped"}, {projection: {status: 1, updated_at: 1, user_id: 1, _id: 1}}) - 注意:如果投影里含
_id,它必须显式出现在索引中,否则 MongoDB 仍要回表取_id
用 $slice 投影控制数组返回长度
真要读数组内容时,别让整个数组进结果。用 $slice 在 projection 里硬限大小,避免传输和解析开销。
- 只取前 5 条日志:
{logs: {$slice: 5}} - 取最后 3 条:
{logs: {$slice: -3}} - 配合索引使用效果更明显:比如有索引
{user_id: 1, created_at: -1},再加$slice投影,就能避开加载全部activities数组
验证是否真的“只走索引”
光写对了不行,得看 MongoDB 实际有没有偷懒。跑 explain("executionStats"),盯住两个字段:
-
executionStats.nReturned应等于executionStats.totalDocsExamined—— 表示没白扫文档 -
executionStats.executionStages.stage是IXSCAN而不是COLLSCAN或FETCH - 如果看到
"indexOnly": true(旧版驱动)或"totalKeysExamined" > 0 && "totalDocsExamined" === 0(新版),说明成功覆盖
容易被忽略的是:哪怕索引字段全齐了,只要 projection 里漏掉某个索引字段、或多写了没索引的字段,MongoDB 就会 fallback 到 FETCH 阶段——这点在调试时最常踩坑。


















