Skip to content

MongoDB - 覆盖查询

本章探讨 MongoDB 中最强大的性能优化技术之一:覆盖查询。

覆盖查询是指完全可以使用索引来满足的查询,而无需检查任何文档。这在以下情况下可能实现:

  • 查询筛选器中使用的所有字段都是索引的一部分。
  • 查询投影中返回的所有字段也都在同一个索引中。

由于索引以高效的数据结构存储在 RAM 中,MongoDB 可以极快地回答覆盖查询。它避免了从磁盘获取文档这个慢得多的过程,而这通常是主要的性能瓶颈。

考虑一个 users 集合,其中包含类似这样的文档:

{
"_id": ObjectId("64f8a55b23e8b1e1ab654321"),
"username": "johndoe",
"status": "active",
"reputation": 250
}

首先,让我们在 status 和 username 字段上创建一个复合索引以支持我们的查询。

// Using mongosh
db.users.createIndex({ status: 1, username: 1 })

现在,我们可以执行一个由该索引“覆盖”的查询。请注意,投影明确请求 username 和 status,并排除了 _id。

db.users.find(
{ status: "active" }, // Query filter uses an indexed field
{ username: 1, status: 1, _id: 0 } // Projection uses only indexed fields
)

要确认查询是否是覆盖的,我们使用 .explain('executionStats') 方法。

db.users.find(
{ status: "active" },
{ username: 1, status: 1, _id: 0 }
).explain('executionStats')

输出将是一个大型 JSON 对象。请查找这些关键指标:

  • 在 queryPlanner.winningPlan.stage 中,您将看到 PROJECTION_COVERED 而不是 PROJECTION_DEFAULT。
  • 其前面的阶段将是 IXSCAN(索引扫描),而不是 COLLSCAN(集合扫描)。
  • 在 executionStats 中,totalDocsExamined 将为 0。这是明确的证明:MongoDB 无需查看任何完整的文档即可返回结果。

默认情况下,MongoDB 总是返回 _id 字段。如果您的索引不包含 _id,但您在投影中忘记明确排除它(_id: 0),则查询将不会被覆盖。MongoDB 将不得不为了获取 _id 而获取整个文档,从而适得其反。

// THIS QUERY IS NOT COVERED because _id is not in the index
db.users.find({ status: "active" }, { username: 1, status: 1 })

虽然在数组字段上的多键索引可以用于覆盖查询,但它有某些限制。如果查询投影了一个用多键索引建立索引的数组字段,则无法使用覆盖查询。