应使用 explain("executionStats") 查看 totalDocsExamined、totalKeysExamined、nReturned;$match 必须置于聚合首阶段;复合索引字段需按等值→范围顺序排列;多键索引易致 totalDocsExamined 虚高,宜用部分索引优化。

看 explain("executionStats") 而不是 explain()
只调用 explain() 只能看到执行阶段(比如 IXSCAN、COLLSCAN),但无法判断到底扫了多少数据。真正要盯住的是 explain("executionStats") 返回的三个核心数字:totalDocsExamined、totalKeysExamined、nReturned。如果 totalDocsExamined 比 nReturned 高出几个数量级(例如 500 万 vs. 200),说明 MongoDB 在索引扫描后做了大量文档级过滤(FETCH + filter),这是典型的“索引未覆盖”信号,不是数据量大,而是索引没建对。
聚合管道里 $match 必须放最前面
哪怕你加了索引,如果 $match 不在第一阶段,MongoDB 就没法提前过滤数据,后续所有阶段($lookup、$group)都得处理全量文档。常见错误包括:
- 把
$project或$addFields放在$match前面 - 在
$lookup后才做主表条件过滤 - 误以为
$match放中间能“复用”上一阶段结果,实际它无法触发索引
正确写法是:所有能下推到集合扫描层的过滤条件,必须塞进第一个 $match,且字段要和索引前缀严格匹配。
复合索引字段顺序必须按“等值→范围”排列
MongoDB 复合索引只支持前缀匹配,顺序错了等于白建。比如查询:
db.orders.aggregate([
{ $match: { status: "shipped", category: "electronics", createdAt: { $gte: ISODate("2025-01-01") } } },
{ $group: { _id: "$region", total: { $sum: "$amount" } } }
])
对应索引必须是 {"status": 1, "category": 1, "createdAt": 1},而不是反过来。否则 status 和 category 就无法跳转,只能靠 createdAt 扫一大片再逐个过滤。额外注意:$in 算伪等值,可放中间;但 $exists、$ne 基本不走索引,除非配合 partialFilterExpression。
totalDocsExamined 居高不下时优先检查多键索引
如果查询字段是数组(如 tags: ["a", "b"]),MongoDB 会自动创建多键索引("ismultikey": true),导致索引膨胀严重、选择性变差。此时 totalDocsExamined 往往虚高。解决路径很明确:
- 确认该字段是否真需要数组语义 —— 若只是存单值,改用字符串或 ObjectId
- 若必须用数组,且高频查询满足某固定子集(如只查
tags: "promo"),就用部分索引:createIndex({ tags: 1 }, { partialFilterExpression: { tags: "promo" } }) - 避免在多键字段上建复合索引的前缀,极易失效
真正卡住性能的,往往不是数据量或服务器配置,而是 totalDocsExamined 和索引字段顺序之间那点微妙的错位——它不会报错,只会默默拖慢整个管道。



















