如何使用 explain() 分析MongoDB的查询性能?
在 MongoDB 中,explain() 是分析查询性能、调试索引使用情况以及优化数据库操作的核心工具。它能告诉你 MongoDB 是如何执行你的查询的(例如:是否使用了索引,扫描了多少文档,耗时多少等)。
以下是使用 explain() 分析 MongoDB 查询性能的完整指南。
1. 基本语法
explain() 可以用于 find(), aggregate(), update(), remove() 等方法。主要有两种调用方式:
方式 A:直接跟在 cursor 后面(最常用)
db.collection.find({ status: "A" }).explain("executionStats")
方式 B:构建 explain 对象(支持更多操作)
db.collection.explain("executionStats").find({ status: "A" })
db.collection.explain("executionStats").update({ status: "A" }, { $set: { status: "B" } })
2. 三种详细模式 (Verbosity Modes)
explain() 接受一个参数来控制输出的详细程度。选择正确的模式非常重要。
queryPlanner(默认)- 行为:MongoDB 优化器分析查询并选择最佳计划,但不执行查询。
- 用途:快速查看 MongoDB 打算使用哪个索引,或者是否存在索引。
- 缺点:看不到实际耗时和扫描的文档数。
executionStats(推荐)- 行为:MongoDB 运行查询优化器,选择计划,并实际执行查询(但不会将结果返回给客户端)。
- 用途:查看实际的执行时间 (
executionTimeMillis)、扫描的文档数与返回文档数的比例。这是性能调优最常用的模式。 - 注意:对于写操作(update/delete),它会执行修改,请在生产环境谨慎使用。
allPlansExecution- 行为:执行查询,并返回获胜计划的统计信息,同时还会返回其他被评估但未被选中的计划的统计信息。
- 用途:当你怀疑 MongoDB 选错了索引,想对比不同索引的性能时使用。
3. 如何解读输出结果 (关键指标)
当你使用 executionStats 模式时,会得到一个巨大的 JSON 对象。你只需要关注以下几个核心部分:
A. executionStats (执行统计)
这是最关键的部分,直接反映性能。
nReturned: 查询返回的文档数量。executionTimeMillis: 查询执行的总耗时(毫秒)。totalKeysExamined: 扫描的索引键数量。totalDocsExamined: 扫描的文档(数据本身)数量。
⚡️ 性能黄金法则:
理想情况下:
nReturned≈totalKeysExamined≈totalDocsExamined
- 如果
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 集合,包含 age 和 username 字段。
案例 1:没有索引 (COLLSCAN)
db.users.find({ age: 25 }).explain("executionStats")
输出片段分析:
"stage": "COLLSCAN",
"nReturned": 5,
"executionTimeMillis": 120,
"totalKeysExamined": 0,
"totalDocsExamined": 100000
- 分析:为了找到 5 个用户,MongoDB 扫描了 10 万个文档 (
totalDocsExamined)。stage是COLLSCAN。 - 结论:性能极差,需要建立索引。
案例 2:使用索引 (IXSCAN)
我们在 age 上建立索引:db.users.createIndex({ age: 1 })。
db.users.find({ age: 25 }).explain("executionStats")
输出片段分析:
"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。
db.users.find({ age: 25 }, { age: 1, _id: 0 }).explain("executionStats")
输出片段分析:
"stage": "IXSCAN", // 注意:没有 FETCH 阶段
"nReturned": 5,
"totalKeysExamined": 5,
"totalDocsExamined": 0 // 竟然是 0!
- 分析:
totalDocsExamined为 0,因为数据直接从索引中获取,不需要去读取原始文档。 - 结论:这是最高效的查询方式。
5. 常见性能问题与优化建议
出现
COLLSCAN:- 原因:查询条件没有命中任何索引。
- 解决:根据查询条件 (
find中的字段) 创建相应的索引。
出现
SORT阶段且内存报错:- 原因:排序操作没有使用索引,且数据量超过了 MongoDB 的内存排序限制 (默认 100MB)。
- 解决:创建包含排序字段的复合索引(例如:
db.collection.createIndex({ age: 1, score: -1 }))。
totalDocsExamined远大于nReturned(即使是 IXSCAN):- 原因:索引区分度不高(例如
gender字段,只有男女两个值)。 - 解决:使用复合索引,将区分度高的字段放在索引的前面。
- 原因:索引区分度不高(例如
正则查询导致全表扫描:
- 原因:使用了左模糊匹配(如
/^abc/走索引,但/abc/不走索引)。 - 解决:避免左模糊,或使用全文搜索引擎(如 Atlas Search 或 Elasticsearch)。
- 原因:使用了左模糊匹配(如
6. 可视化工具推荐
如果你觉得 JSON 输出太难读,推荐使用 MongoDB Compass。
- 打开 Compass 连接数据库。
- 输入查询条件。
- 点击 "Explain Plan" 标签页。
- 它会以图形化的方式展示 pipeline,清晰地标出是否使用了索引 (IXSCAN) 还是全表扫描 (COLLSCAN)。