← 返回题目列表

MongoDB 覆盖查询 Covered Query 是什么?Projection 为什么会影响性能?

中等 第 22 / 31 题 更新于 2026/07/30
MongoDBCovered QueryProjection索引

简化版

覆盖查询指查询条件和返回字段都能从索引中得到,不需要回表读取完整文档。Projection 能减少返回字段,但只有返回字段也在索引里时,才可能形成真正的覆盖查询。

详细版

MongoDB 查询通常先扫索引定位候选文档,再读取文档内容。如果查询需要的字段都在索引键中,就可以避免读取完整文档。

  • 覆盖查询需要过滤字段、排序字段、返回字段都能由索引满足。
  • Projection 只控制返回哪些字段,不保证一定覆盖;字段不在索引里仍要读取文档。
  • 默认返回 _id,如果 _id 不在索引里,可能破坏覆盖,需要显式排除 _id
  • 大文档场景下覆盖查询收益明显,因为可以少读很多文档页。
  • 不能为了覆盖把索引做得过宽,写入成本和索引体积也要权衡。

完整版教学

一、为什么读取完整文档可能很贵

MongoDB 的索引通常比完整文档小得多。一次查询如果通过索引找到 1000 条候选记录,但每条文档都有几十 KB 的描述、图片元数据或嵌套数组,那么回表读取完整文档会带来明显 I/O 和内存压力。覆盖查询的价值就是:如果你只需要 namepricestatus 这些小字段,而它们都在索引里,数据库可以直接从索引返回结果。这样少走一步文档读取,延迟和资源消耗都会更可控。

db.products.createIndex({ categoryId: 1, status: 1, price: 1, name: 1 })

记忆钩子:Projection 是“少带回给客户端”,Covered Query 是“数据库根本不用去读原文档”。

二、覆盖查询需要满足哪些条件

覆盖查询不是只要建了索引就行,而是查询条件、排序字段、返回字段都能被同一个索引满足。比如索引是 { categoryId:1, status:1, price:1, name:1 },查询按分类和状态过滤,按价格排序,只返回 nameprice,就有机会覆盖。如果返回了 description,而它不在索引里,数据库仍要读取完整文档。覆盖查询本质上是把索引当成一个“小型只读表”来用。

db.products.find(
  { categoryId: 12, status: "ON_SALE" },
  { _id: 0, name: 1, price: 1 }
).sort({ price: 1 })

三、为什么 _id 经常让覆盖失败

MongoDB 默认会返回 _id 字段。很多人查询时写了 projection,只包含 nameprice,却忘了 _id 默认仍会返回。如果你的索引里没有 _id,为了返回 _id,数据库可能还是需要读取文档,从而不再是覆盖查询。解决方式是显式写 { _id: 0 },或者在确实需要 _id 时把它纳入索引设计。这个小细节在面试中很常被追问,因为它能区分是否真的做过 MongoDB 查询优化。

// 注意 _id: 0
db.products.find(
  { categoryId: 12, status: "ON_SALE" },
  { _id: 0, name: 1, price: 1 }
)

四、Projection 和覆盖查询不是一回事

Projection 的作用是告诉数据库最终返回哪些字段给客户端,它可以减少网络传输和应用层反序列化成本。但如果这些字段不都在索引里,数据库内部仍可能要读取完整文档,再把不需要的字段裁掉。覆盖查询更进一步,连文档读取都省掉。两者经常一起出现,但概念不同:Projection 是输出裁剪,覆盖查询是执行路径优化。

能力ProjectionCovered Query
少返回字段
一定不读文档
依赖索引字段完整性不一定必须
主要收益网络与反序列化I/O、内存、延迟

五、用数字看覆盖查询的收益

假设商品文档平均 20KB,索引项平均 100B。一个列表页查询扫描 5000 条候选,如果需要回表,理论读取文档数据量约 100MB;如果覆盖查询只扫索引,扫描数据量可能约 0.5MB。真实情况还会受缓存、压缩、页布局影响,但数量级差异足以解释为什么大文档列表接口非常适合考虑覆盖查询。尤其是移动端列表页,只展示标题、价格、封面 ID 时,不应每次读取大段详情。

5000 条 × 20KB 文档 ≈ 100MB
5000 条 × 100B 索引项 ≈ 0.5MB
差距约 200 倍

六、索引不能为了覆盖无限变宽

覆盖查询有诱惑:既然返回字段在索引里更快,那是不是把所有列表字段都塞进复合索引?答案是否定的。索引越宽,存储越大,写入维护越慢,缓存能容纳的索引页越少。更合理的做法是只覆盖真正高频、字段少、延迟敏感的查询;对于低频后台查询,回表完全可以接受。性能优化不是把所有查询都做到极致,而是把成本花在核心路径上。

// 不建议为了偶发需求把 description、specs 大字段塞进列表索引
db.products.createIndex({ categoryId: 1, status: 1, price: 1, name: 1 })

七、常见误区与追问

  • 误区:用了 Projection 就一定是覆盖查询。 Projection 只是裁剪返回字段,字段不在索引里仍可能读取文档。
  • 误区:索引字段越多越好。 宽索引会增加写入成本和缓存压力,要围绕高频查询设计。
  • 误区:忘记 _id 不影响覆盖。 _id 默认返回,如果不在索引中,可能导致回表读取。
  • 追问:怎么判断是否覆盖?explain("executionStats") 看执行阶段和文档读取数量,关注是否避免 FETCH。
  • 追问:大文档为什么更适合覆盖查询? 因为读取完整文档的 I/O 成本高,索引项小得多。

八、加强记忆

覆盖查询可以记成“只看目录,不翻正文”。索引像目录,如果目录里已经有过滤、排序和返回字段,就不用打开原文档。Projection 只是告诉最终要哪些字段,不等于不翻正文;真正覆盖要靠索引字段完整满足查询。面试回答时把 _id 默认返回、宽索引代价和 explain 验证一起讲出来,就显得很稳。