基于本文回答
0
评论

如何使用 explain() 分析MongoDB的查询性能?

知识点图片

在 MongoDB 中,explain() 是分析查询性能、调试索引使用情况以及优化数据库操作的核心工具。它能告诉你 MongoDB 是如何执行你的查询的(例如:是否使用了索引,扫描了多少文档,耗时多少等)。

以下是使用 explain() 分析 MongoDB 查询性能的完整指南。


1. 基本语法

explain() 可以用于 find(), aggregate(), update(), remove() 等方法。主要有两种调用方式:

方式 A:直接跟在 cursor 后面(最常用)

javascript
db.collection.find({ status: "A" }).explain("executionStats")

方式 B:构建 explain 对象(支持更多操作)

javascript
db.collection.explain("executionStats").find({ status: "A" })
db.collection.explain("executionStats").update({ status: "A" }, { $set: { status: "B" } })

2. 三种详细模式 (Verbosity Modes)

explain() 接受一个参数来控制输出的详细程度。选择正确的模式非常重要。

  1. queryPlanner (默认)

    • 行为:MongoDB 优化器分析查询并选择最佳计划,但不执行查询。
    • 用途:快速查看 MongoDB 打算使用哪个索引,或者是否存在索引。
    • 缺点:看不到实际耗时和扫描的文档数。
  2. executionStats (推荐)

    • 行为:MongoDB 运行查询优化器,选择计划,并实际执行查询(但不会将结果返回给客户端)。
    • 用途:查看实际的执行时间 (executionTimeMillis)、扫描的文档数与返回文档数的比例。这是性能调优最常用的模式。
    • 注意:对于写操作(update/delete),它会执行修改,请在生产环境谨慎使用。
  3. allPlansExecution

    • 行为:执行查询,并返回获胜计划的统计信息,同时还会返回其他被评估但未被选中的计划的统计信息。
    • 用途:当你怀疑 MongoDB 选错了索引,想对比不同索引的性能时使用。

3. 如何解读输出结果 (关键指标)

当你使用 executionStats 模式时,会得到一个巨大的 JSON 对象。你只需要关注以下几个核心部分:

A. executionStats (执行统计)

这是最关键的部分,直接反映性能。

  • nReturned: 查询返回的文档数量。
  • executionTimeMillis: 查询执行的总耗时(毫秒)。
  • totalKeysExamined: 扫描的索引键数量。
  • totalDocsExamined: 扫描的文档(数据本身)数量。

⚡️ 性能黄金法则:

理想情况下:nReturnedtotalKeysExaminedtotalDocsExamined

  • 如果 totalDocsExamined 远大于 nReturned:说明数据库扫描了很多文档但只用到了很少一部分,通常意味着缺少索引或索引效率低。
  • 如果 totalKeysExamined 远大于 nReturned:说明索引被使用了,但索引的选择性不好(比如在布尔字段上建索引)。

B. queryPlanner.winningPlan (获胜计划)

这里展示了 MongoDB 决定如何执行查询。重点看 stage 字段。

常见的 Stage (阶段):

Stage 名称 含义 评价
COLLSCAN Collection Scan (全表扫描)。必须遍历整个集合才能找到数据。 🔴 (数据量大时必须优化)
IXSCAN Index Scan (索引扫描)。使用了索引。 🟢
FETCH 根据索引位置去抓取完整文档数据。 🟡 正常 (通常伴随 IXSCAN)
PROJECTION_COVERED Covered Query (覆盖查询)。所需字段全在索引中,无需回表查文档。 🌟 极好 (性能最高)
SORT 在内存中进行排序(未使用索引排序)。 🔴 (受 100MB 内存限制)

4. 实战分析案例

假设有一个 users 集合,包含 ageusername 字段。

案例 1:没有索引 (COLLSCAN)

javascript
db.users.find({ age: 25 }).explain("executionStats")

输出片段分析:

json
"stage": "COLLSCAN",
"nReturned": 5,
"executionTimeMillis": 120,
"totalKeysExamined": 0,
"totalDocsExamined": 100000
  • 分析:为了找到 5 个用户,MongoDB 扫描了 10 万个文档 (totalDocsExamined)。stageCOLLSCAN
  • 结论:性能极差,需要建立索引。

案例 2:使用索引 (IXSCAN)

我们在 age 上建立索引:db.users.createIndex({ age: 1 })

javascript
db.users.find({ age: 25 }).explain("executionStats")

输出片段分析:

json
"stage": "FETCH",
"inputStage": {
  "stage": "IXSCAN",
  "keyPattern": { "age": 1 }
},
"nReturned": 5,
"executionTimeMillis": 1,
"totalKeysExamined": 5,
"totalDocsExamined": 5
  • 分析stage 变成了 IXSCAN + FETCH。扫描了 5 个索引键,扫描了 5 个文档,返回了 5 个文档。
  • 结论:性能非常高。

案例 3:覆盖查询 (Covered Query)

我们只查询 age 字段(该字段已在索引中),并排除 _id

javascript
db.users.find({ age: 25 }, { age: 1, _id: 0 }).explain("executionStats")

输出片段分析:

json
"stage": "IXSCAN", // 注意:没有 FETCH 阶段
"nReturned": 5,
"totalKeysExamined": 5,
"totalDocsExamined": 0  // 竟然是 0!
  • 分析totalDocsExamined 为 0,因为数据直接从索引中获取,不需要去读取原始文档。
  • 结论:这是最高效的查询方式。

5. 常见性能问题与优化建议

  1. 出现 COLLSCAN

    • 原因:查询条件没有命中任何索引。
    • 解决:根据查询条件 (find 中的字段) 创建相应的索引。
  2. 出现 SORT 阶段且内存报错

    • 原因:排序操作没有使用索引,且数据量超过了 MongoDB 的内存排序限制 (默认 100MB)。
    • 解决:创建包含排序字段的复合索引(例如:db.collection.createIndex({ age: 1, score: -1 }))。
  3. totalDocsExamined 远大于 nReturned (即使是 IXSCAN)

    • 原因:索引区分度不高(例如 gender 字段,只有男女两个值)。
    • 解决:使用复合索引,将区分度高的字段放在索引的前面。
  4. 正则查询导致全表扫描

    • 原因:使用了左模糊匹配(如 /^abc/ 走索引,但 /abc/ 不走索引)。
    • 解决:避免左模糊,或使用全文搜索引擎(如 Atlas Search 或 Elasticsearch)。

6. 可视化工具推荐

如果你觉得 JSON 输出太难读,推荐使用 MongoDB Compass

  1. 打开 Compass 连接数据库。
  2. 输入查询条件。
  3. 点击 "Explain Plan" 标签页。
  4. 它会以图形化的方式展示 pipeline,清晰地标出是否使用了索引 (IXSCAN) 还是全表扫描 (COLLSCAN)。
右滑查看面试常问