外观
MongoDB 执行计划分析
执行计划基本概念
1. 查询阶段
MongoDB 查询执行计划包含多个阶段,每个阶段负责不同的查询操作:
| 阶段 | 描述 |
|---|---|
| COLLSCAN | 全集合扫描,遍历集合中的所有文档 |
| IXSCAN | 索引扫描,使用索引查找文档 |
| FETCH | 从集合中获取文档 |
| SORT | 排序操作 |
| PROJECTION | 投影操作,筛选返回的字段 |
| AGGREGATE | 聚合操作 |
| LIMIT | 限制返回结果数量 |
| SKIP | 跳过指定数量的文档 |
2. 执行统计信息
执行计划包含以下关键统计信息:
- executionTimeMillis:查询执行时间,单位毫秒
- totalKeysExamined:检查的索引键数量
- totalDocsExamined:检查的文档数量
- nReturned:返回的文档数量
- winningPlan:MongoDB 选择的最优执行计划
- rejectedPlans:MongoDB 考虑但未选择的执行计划
使用 explain() 方法分析执行计划
1. explain() 方法的使用
基本语法:
javascript
db.collection.find({ query }).explain([verbose])verbose 参数选项:
| 选项 | 描述 |
|---|---|
| "queryPlanner" | 只返回查询计划信息(默认) |
| "executionStats" | 返回查询计划和执行统计信息 |
| "allPlansExecution" | 返回查询计划、执行统计信息和所有考虑的计划 |
2. 示例
基本查询分析:
javascript
// 连接到 MongoDB
mongosh
// 使用 explain() 分析查询计划
use mydb
db.mycollection.find({ name: "test" }).explain("executionStats")索引查询分析:
javascript
// 创建索引
db.mycollection.createIndex({ name: 1 })
// 分析索引查询计划
db.mycollection.find({ name: "test" }).explain("executionStats")复杂查询分析:
javascript
// 分析复杂查询计划
db.mycollection.find({ name: "test", age: { $gt: 30 } }).sort({ createdAt: -1 }).limit(10).explain("executionStats")执行计划输出解读
1. queryPlanner 部分
json
{
"queryPlanner": {
"plannerVersion": 1,
"namespace": "mydb.mycollection",
"indexFilterSet": false,
"parsedQuery": {
"name": {
"$eq": "test"
}
},
"winningPlan": {
"stage": "FETCH",
"inputStage": {
"stage": "IXSCAN",
"keyPattern": {
"name": 1
},
"indexName": "name_1",
"isMultiKey": false,
"multiKeyPaths": {
"name": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"name": [
"["test", "test"]"
]
}
}
},
"rejectedPlans": []
}
}2. executionStats 部分
json
{
"executionStats": {
"executionSuccess": true,
"nReturned": 1,
"executionTimeMillis": 10,
"totalKeysExamined": 1,
"totalDocsExamined": 1,
"executionStages": {
"stage": "FETCH",
"nReturned": 1,
"executionTimeMillisEstimate": 5,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"docsExamined": 1,
"alreadyHasObj": 0,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 1,
"executionTimeMillisEstimate": 2,
"works": 2,
"advanced": 1,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"keyPattern": {
"name": 1
},
"indexName": "name_1",
"isMultiKey": false,
"multiKeyPaths": {
"name": []
},
"isUnique": false,
"isSparse": false,
"isPartial": false,
"indexVersion": 2,
"direction": "forward",
"indexBounds": {
"name": [
"["test", "test"]"
]
},
"keysExamined": 1,
"seeks": 1,
"dupsTested": 0,
"dupsDropped": 0,
"seenInvalidated": 0,
"matchTested": 0
}
}
}
}常见执行计划问题及优化
1. 全集合扫描(COLLSCAN)
症状:
- 执行计划中出现
"stage": "COLLSCAN" totalDocsExamined远大于nReturned- 执行时间长
解决方案:
- 创建合适的索引,避免全集合扫描
- 优化查询条件,确保使用索引
- 考虑使用覆盖索引,减少回表查询
2. 索引扫描但扫描文档过多
症状:
- 执行计划中出现
"stage": "IXSCAN" totalKeysExamined远大于nReturned- 执行时间长
解决方案:
- 优化索引设计,创建更精确的索引
- 调整查询条件,确保使用最匹配的索引
- 使用
hint()指定索引
3. 排序操作(SORT)
症状:
- 执行计划中出现
"stage": "SORT" - 执行时间长
解决方案:
- 创建包含排序字段的复合索引
- 限制排序结果集大小
- 考虑在应用层进行排序
4. 跳过大量文档(SKIP)
症状:
- 执行计划中出现
"stage": "SKIP" skip参数值较大- 执行时间长
解决方案:
- 使用范围查询替代
skip(),例如使用{ _id: { $gt: ObjectId(...) } } - 考虑使用游标分页
- 限制分页深度
执行计划分析工具
1. MongoDB Compass
使用方法:
- 打开 MongoDB Compass
- 连接到 MongoDB 实例
- 导航到 "Collections" 标签页
- 选择集合,点击 "Filter" 按钮
- 输入查询条件,点击 "Explain Plan" 按钮
- 查看执行计划和统计信息
2. mongosh
使用方法:
- 连接到 MongoDB
- 使用
explain()方法分析查询计划 - 查看输出结果,分析执行计划
3. 第三方工具
- MongoDB Atlas:提供可视化的执行计划分析工具
- Robo 3T:支持执行计划分析
- Studio 3T:提供高级的执行计划分析功能
执行计划分析最佳实践
1. 定期分析执行计划
- 定期分析慢查询的执行计划
- 监控查询性能,及时发现性能问题
- 新查询上线前分析执行计划
2. 关注关键指标
- totalDocsExamined / nReturned:比率越低越好
- executionTimeMillis:执行时间越短越好
- winningPlan:优先选择使用索引的执行计划
3. 优化索引设计
- 根据查询模式创建合适的索引
- 避免过多索引,每个索引都会增加写操作开销
- 使用覆盖索引,减少回表查询
- 定期重建索引,提高索引性能
4. 优化查询语句
- 限制结果集大小,使用
limit()和skip() - 使用投影,只返回需要的字段
- 避免全集合扫描,使用索引
- 优化排序操作,使用索引排序
常见问题(FAQ)
Q1: 如何确定查询是否使用了索引?
A1: 可以通过以下方法确定查询是否使用了索引:
- 使用
explain("executionStats")分析执行计划 - 检查执行计划中的
stage字段,若为IXSCAN则使用了索引,若为COLLSCAN则未使用索引 - 查看
totalKeysExamined和totalDocsExamined字段,若totalKeysExamined大于 0 则使用了索引
Q2: 如何选择合适的索引?
A2: 选择合适索引的方法:
- 分析查询模式,确定查询中使用的字段
- 优先为频繁查询的字段创建索引
- 考虑查询的排序和分组字段
- 避免创建过多索引,每个索引都会增加写操作开销
- 使用
explain()分析索引效果
Q3: 如何优化慢查询?
A3: 优化慢查询的方法:
- 分析执行计划,识别性能瓶颈
- 优化索引设计,创建合适的索引
- 优化查询语句,避免全集合扫描
- 限制结果集大小,使用
limit()和skip() - 使用投影,只返回需要的字段
Q4: 如何处理复杂查询?
A4: 处理复杂查询的方法:
- 分解复杂查询为多个简单查询
- 使用聚合管道优化复杂查询
- 考虑使用
$lookup替代多次查询 - 优化索引设计,支持复杂查询
Q5: 如何验证索引的有效性?
A5: 验证索引有效性的方法:
- 使用
explain()分析查询计划,检查是否使用了索引 - 监控查询执行时间,查看索引是否提高了查询性能
- 查看索引使用率,定期清理未使用的索引
- 测试不同索引的效果,选择最优索引
Q6: 如何分析聚合查询的执行计划?
A6: 分析聚合查询执行计划的方法:
- 使用
db.collection.aggregate(pipeline).explain("executionStats")分析聚合查询计划 - 查看聚合管道的每个阶段的执行情况
- 优化聚合管道,使用
$match尽早过滤数据 - 优化聚合操作,使用索引支持聚合查询
示例:优化查询执行计划
问题描述
查询 db.users.find({ age: { $gt: 30 } }).sort({ createdAt: -1 }) 执行时间过长,需要优化。
分析执行计划
javascript
// 分析执行计划
db.users.find({ age: { $gt: 30 } }).sort({ createdAt: -1 }).explain("executionStats")执行计划输出:
json
{
"executionStats": {
"executionSuccess": true,
"nReturned": 100,
"executionTimeMillis": 500,
"totalKeysExamined": 0,
"totalDocsExamined": 100000,
"executionStages": {
"stage": "SORT",
"nReturned": 100,
"executionTimeMillisEstimate": 450,
"works": 100002,
"advanced": 100,
"needTime": 99901,
"needYield": 0,
"saveState": 781,
"restoreState": 781,
"isEOF": 1,
"sortPattern": {
"createdAt": -1
},
"memUsage": 12582912,
"memLimit": 33554432,
"inputStage": {
"stage": "COLLSCAN",
"filter": {
"age": {
"$gt": 30
}
},
"nReturned": 50000,
"executionTimeMillisEstimate": 100,
"works": 100001,
"advanced": 50000,
"needTime": 50000,
"needYield": 0,
"saveState": 781,
"restoreState": 781,
"isEOF": 1,
"docsExamined": 100000,
"executionTimeMillisEstimate": 100
}
}
}
}分析问题:
- 执行计划显示
COLLSCAN(全集合扫描) totalDocsExamined为 100000,nReturned为 100,比率过高executionTimeMillis为 500ms,执行时间过长- 存在
SORT阶段,消耗大量内存
优化方案
创建复合索引:
javascript
// 创建复合索引,包含查询字段和排序字段
db.users.createIndex({ age: 1, createdAt: -1 })重新分析执行计划:
javascript
db.users.find({ age: { $gt: 30 } }).sort({ createdAt: -1 }).explain("executionStats")优化后的执行计划:
json
{
"executionStats": {
"executionSuccess": true,
"nReturned": 100,
"executionTimeMillis": 10,
"totalKeysExamined": 100,
"totalDocsExamined": 100,
"executionStages": {
"stage": "FETCH",
"nReturned": 100,
"executionTimeMillisEstimate": 5,
"works": 101,
"advanced": 100,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"docsExamined": 100,
"inputStage": {
"stage": "IXSCAN",
"nReturned": 100,
"executionTimeMillisEstimate": 2,
"works": 101,
"advanced": 100,
"needTime": 0,
"needYield": 0,
"saveState": 0,
"restoreState": 0,
"isEOF": 1,
"keyPattern": {
"age": 1,
"createdAt": -1
},
"indexName": "age_1_createdAt_-1",
"isMultiKey": false,
"direction": "forward",
"indexBounds": {
"age": [
"(30.0, inf.0]"
],
"createdAt": [
"[MaxKey, MinKey]"
]
},
"keysExamined": 100,
"seeks": 1
}
}
}
}优化效果:
- 执行计划显示
IXSCAN(索引扫描) totalDocsExamined为 100,nReturned为 100,比率优化executionTimeMillis为 10ms,执行时间大幅减少- 不再有
SORT阶段,使用索引排序
