根本原因是未满足覆盖索引全部条件:索引必须包含查询、投影及排序字段,且需显式排除_id、禁用$text/$regex等强制加载操作;验证须以explain("executionstats")中iscovered为true且totaldocsexamined=0为准。

为什么IXSCAN后还出现FETCH
根本原因不是“索引没建对”,而是查询没满足覆盖索引(covered query)的全部条件。MongoDB 只有在**索引完全包含查询所需的所有字段 + 显式排除_id + 无无法下推的操作**时,才跳过 FETCH 阶段直接返回结果。
常见错误现象:explain("executionStats") 显示 executionStages.stage === "FETCH",即使你只查了索引里的字段,比如:
db.orders.find({ status: "paid" }, { status: 1, amount: 1 }).explain("executionStats")
——只要没排除 _id,或索引缺了 amount,就一定会 FETCH。
-
_id默认强制返回,必须显式设为{ _id: 0 }才可能覆盖 - 索引字段必须完整覆盖:查询条件字段 + 投影字段 + 排序字段(如有
.sort()) - 用了
$regex(带i标志)、$text、$where、$expr等,会强制 FETCH - 复合索引中字段顺序无关覆盖性,但缺失任一字段即失效(例如投影含
c,索引却是{a: 1, b: 1})
如何确认是否真走了覆盖索引
别信 stage === "IXSCAN",那是假阳性。MongoDB 官方唯一认定依据是 executionStats.executionStages.isCovered === true,且 totalDocsExamined === 0。
执行时必须用 "executionStats" 模式,"queryPlanner" 不返回 isCovered 字段(MongoDB 5.0+ 已移除):
db.collection.find({ a: 1 }, { a: 1, b: 1, _id: 0 }).explain("executionStats")
- 如果
isCovered是false,再看executionStages.stage是否嵌套了FETCH -
extraInfo.covered是旧版驱动遗留字段,新版 MongoDB Server 已不输出,不可信 -
totalDocsExamined > 0就说明文档被读取过,覆盖失败
排除_id后还FETCH的隐蔽原因
显式写 { _id: 0 } 是必要条件,但不保证成功。以下情况仍会触发 FETCH:
- 排序字段未包含在索引中:如
.sort({ createdAt: -1 })但索引是{ status: 1, amount: 1 } - 索引是稀疏(
sparse: true)或部分索引(partialFilterExpression),而当前查询文档不满足过滤条件,MongoDB 得 FETCH 后判断是否该返回 - 查询用了
$exists: true且字段可空——MongoDB 无法仅靠索引键判断是否存在,必须 FETCH 文档验证(尤其在多键索引中) - 聚合管道里用了
$project但输出字段含计算逻辑(如{ total: { $add: ["$a", "$b"] } }),索引无法支撑表达式计算
调试时最容易忽略的一点
很多人只盯着 winningPlan 的第一层 stage,却没展开看嵌套结构。FETCH 经常藏在 inputStage.inputStage.stage 里。例如:
"winningPlan": {
"stage": "PROJECTION",
"inputStage": {
"stage": "FETCH",
"inputStage": { "stage": "IXSCAN", ... }
}
}
这种结构下,IXSCAN 确实发生了,但紧接着就是 FETCH——说明索引只用于定位,没用于返回数据。真正关键的是最内层是否能闭环,而不是最外层叫什么名字。











