覆盖索引需满足:查询的filter和projection字段(显式排除_id)必须全部包含在同一复合索引中;验证时totalDocsExamined必须为0且执行阶段不能含FETCH。

确认查询是否满足覆盖索引前提条件
覆盖索引不是自动生效的魔法开关,MongoDB 5.0 严格要求:查询中 所有 filter 字段 + 所有 projection 字段 必须全部包含在同一个复合索引中,且 显式排除 _id 字段,否则必然触发 FETCH 阶段回表。
例如,执行 db.orders.find({status: "shipped", region: "CN"}, {orderId: 1, amount: 1}),若索引为 {status: 1, region: 1, orderId: 1, amount: 1},但未在 projection 中写明 _id: 0,则 MongoDB 仍会默认返回 _id 并因该字段不在索引中而强制回表。
这一步漏掉 _id: 0 就破功,务必检查 projection 是否完整覆盖且不含隐式字段。
构建满足覆盖条件的复合索引
方法一:按等值过滤字段优先、范围字段居中、投影字段收尾的顺序建索引。
例如,常见查询是 status = "paid" 且 created_at > ISODate("2024-01-01"),同时只取 userId 和 total;那么应创建索引:db.payments.createIndex({"status": 1, "created_at": 1, "userId": 1, "total": 1})。
注意:created_at 是范围查询字段,它后面不能再跟其他用于过滤的字段(如 type: 1),否则 type 无法被索引下推,会导致扫描膨胀。
方法二:用部分索引进一步缩小索引体积,仅覆盖高频有效子集。
比如只对 status ∈ ["paid", "refunded"] 的订单做覆盖查询,可建:db.orders.createIndex({"status": 1, "amount": 1}, {partialFilterExpression: {status: {$in: ["paid", "refunded"]}}})。这样索引条目更少,内存占用更低,B树层级更浅。
验证覆盖是否真实生效
第一步:执行带 explain("executionStats") 的查询语句。
第二步:检查返回结果中的两个硬性指标:
① executionStats.totalDocsExamined 必须为 0;
② executionStats.executionStages.stage 必须是 IXSCAN 或 PROJECTION_COVERED,且其子节点不能出现 FETCH。
如果看到 stage: "IXSCAN" 下嵌套了 "stage": "FETCH",说明表面走索引,实际仍在回表——这种假覆盖比不建索引更危险,因为容易误判性能瓶颈。
别信旧版驱动残留的 extraInfo.covered 字段,MongoDB 5.0 已移除该字段,仅依赖上述两项指标判断。
聚合管道中启用覆盖查询
仅当管道严格为 $match → $project 两级,且中间无任何其他阶段时,才可能触发覆盖。
执行 db.logs.aggregate([{$match: {level: "ERROR", service: "auth"}}, {$project: {timestamp: 1, message: 1, _id: 0}}]),必须确保索引包含 {level: 1, service: 1, timestamp: 1, message: 1} 全部四字段。
$sort、$group、$lookup、$unwind 中任意一个出现,都会强制加载完整文档,覆盖失效。
这一步中,哪怕只加一个 $sort: {timestamp: -1},整个管道就立刻失去覆盖能力,MongoDB 会跳转到 FETCH 阶段读取原始文档再排序。


















