覆盖查询省掉了什么
普通索引查询要分两步:先在索引里找到文档位置,再回表读取完整文档。如果查询的过滤字段与返回字段全部来自同一个索引,引擎就不必回表,这就是覆盖查询。判断标准很直接:用 explain 看执行统计里读取的文档数是否为 0,是则说明完全没有访问集合本体。
基本信息
| 成立条件 | 过滤字段与投影字段都在同一个索引里,且显式排除 _id |
|---|---|
| 验证方式 | explain("executionStats") 中 totalDocsExamined 为 0 |
| 收益场景 | 等值点查、小范围扫描上加分明显 |
| 失效原因 | 多键(数组)索引无法支撑覆盖;投影里带上 _id 也会回表 |
| 适用版本 | 各版本行为一致,字段名在 5.0 前后略有差异 |
典型使用场景
- 高频点查接口:只按主键或业务键取少量字段,做到不回表
- 统计与计数:只取索引里已有的字段做汇总
- 列表页精简字段:列表只需要标识与状态时,用覆盖查询避免读整文档
- 降低大文档读取成本:文档体积大时收益尤其明显
- 容量紧张的实例:减少读取压力,间接降低缓存与 IO 需求
示例
示例
// 建立能盖住过滤与返回字段的复合索引
db.orders.createIndex({ sku: 1, qty: 1 })
// 投影必须包含索引内字段,并显式排除 _id
db.orders.find({ sku: "A-1" }, { sku: 1, qty: 1, _id: 0 }).explain("executionStats")
// 验证要点:看执行统计里的文档读取数
// 读取文档数为 0 表示没有回表,即覆盖查询成立
// db.orders.find({ sku: "A-1" }, { sku: 1, qty: 1, _id: 0 }).explain("executionStats").executionStats.totalDocsExamined成立与失效对照
| 情况 | 是否覆盖 | 原因 |
|---|---|---|
| 过滤与返回字段都在索引内且排除 _id | 成立 | 无需读取集合文档 |
| 投影里包含 _id | 失效 | _id 不在该索引内,必须回表 |
| 返回索引外的字段 | 失效 | 需要读取完整文档补字段 |
| 索引建在数组字段上(多键索引) | 失效 | 多键索引无法提供完整字段值 |
| 索引内字段用了不同排序方向 | 视情况 | 方向不影响覆盖能力,但影响范围扫描效率 |
覆盖查询的收益来自省掉回表读取:等值点查或返回少量字段的场景最容易见效,全量扫描类需求即使覆盖也不能替代聚合与预计算。
为追求覆盖而不断加宽索引会推高写入放大与磁盘占用:索引宽度和查询收益要一起评估,别让一个查询优化拖慢整体写入。