零io覆盖查询需同时满足三条件:查询字段、投影字段(含显式_id:0)均在同索引中,且操作符非$ne/$regex等黑名单;explain中stage为projection且totaldocsexamined为0。

能实现零IO,但前提是严格满足覆盖条件——索引必须包含查询条件字段 + 投影字段,且 _id 必须显式排除。不满足任一条件,MongoDB 就会触发文档 FETCH,IO 就回来了。
覆盖查询成立的三个硬性条件
覆盖查询不是“有索引就行”,而是三者缺一不可:
- 查询条件中所有字段(
$eq、$gt等)必须全部出现在同一个索引的keyPattern中 - 投影中返回的所有字段(包括
_id)也必须全部存在于该索引中 -
_id: 0必须显式声明——哪怕你没写_id,MongoDB 默认仍会尝试读取它,破坏覆盖性
例如:索引为 { status: 1, amount: 1 },则以下查询才覆盖:
db.orders.find({ status: "shipped" }, { status: 1, amount: 1, _id: 0 })
漏掉 _id: 0,或投影里多加一个 name: 1(但索引不含 name),就立刻退化为 IXSCAN + FETCH。
explain 中如何确认真的零IO
执行 explain("executionStats") 后,盯住这三个字段:
-
stage必须是PROJECTION(不是FETCH) -
totalDocsExamined必须为0(关键指标) -
executionTimeMillis通常极低(docsReturned ≈totalKeysExamined
如果看到 stage: "FETCH" 或 totalDocsExamined > 0,说明索引结构或投影写法有问题,不是覆盖查询。
复合索引字段顺序直接影响覆盖能力
字段顺序不是随便排的,它决定哪些查询能走覆盖路径:
- 等值字段(
$eq)放最左,比如status: "paid" - 范围字段(
$gt、$in)紧跟其后,比如created_at: { $gt: ... } - 排序字段(
.sort())再往后,比如amount: -1 - 最后才是纯投影字段(只用于返回,不参与过滤/排序),比如
customer_id: 1
错误示例:索引 { amount: 1, status: 1 } 无法覆盖 find({ status: "paid" }) —— 因为 status 不在前缀位置,索引无法高效跳转。
容易被忽略的坑:正则、$ne、$not 都会杀死覆盖
即使索引和投影完全匹配,某些操作符也会强制回表:
-
$regex以非 ^ 开头(如.*abc)时,索引只能做前缀扫描,后续仍需 FETCH 文档验证 -
$ne、$not、$exists: false等否定操作符不支持覆盖查询 - 使用
$text或地理空间查询时,覆盖规则不适用——它们走的是专用索引路径
这类查询在 explain 中表现为 stage: "TEXT" / "GEO_NEAR" + totalDocsExamined > 0,别被 IXSCAN 假象迷惑。
真正零IO的关键不在“建了多少索引”,而在每次查询前是否逐字核对:条件字段全在索引里吗?投影字段全在索引里吗?_id: 0 写了吗?操作符是不是黑名单里的?少检查一步,IO 就悄悄回来了。











