MongoDB查询性能的瓶颈往往隐藏在查询执行计划中。通过explain()获取的executionStats提供了查询执行的完整剖析,是诊断性能问题的"X光片"。本文将系统阐述执行计划的核心指标,提供可落地的诊断方法,帮助您快速定位查询瓶颈,将慢查询优化30%以上。
一、查询执行计划基础:为什么必须关注executionStats
1.1 查询优化器的工作原理
MongoDB查询优化器执行三步决策:
- 候选计划生成:为查询生成多个可能的执行计划
- 计划评估:在测试集上运行各计划,评估成本
- 计划选择:选择成本最低的计划作为最终执行方案
关键洞察:优化器可能选择次优计划,尤其当索引统计信息过期时。
1.2 executionStats vs queryPlanner
| 指标 | queryPlanner | executionStats | allPlansExecution |
|---|---|---|---|
| 数据范围 | 仅计划信息 | 计划+执行统计 | 计划+所有执行路径统计 |
| 执行开销 | 低(不执行查询) | 高(执行查询) | 最高 |
| 核心用途 | 分析索引选择 | 性能问题诊断 | 深度调优 |
| 适用场景 | 开发环境预检 | 生产问题排查 | 复杂查询优化 |
为什么必须用executionStats :
仅看
queryPlanner会遗漏关键问题(如索引碎片、数据分布不均),执行统计是唯一能反映真实性能的指标。
二、获取执行计划:正确使用explain()方法
2.1 基本语法
javascript
// 获取执行统计(推荐)
db.collection.explain("executionStats").find({ ... });
// 获取所有执行路径统计(高级诊断)
db.collection.explain("allPlansExecution").find({ ... });
2.2 重要参数说明
| 参数 | 效果 | 推荐场景 |
|---|---|---|
maxTimeMS |
限制查询执行时间 | 防止慢查询阻塞诊断 |
verbosity |
指定返回级别(默认queryPlanner) | 必须设为executionStats |
comment |
添加注释(用于监控识别) | 生产环境调优 |
2.3 避坑指南
-
错误1 :仅用
db.collection.find().explain()
→ 返回queryPlanner,无执行统计! -
正确做法 :
javascriptdb.collection.explain("executionStats").find({ status: "active" }); -
错误2 :在高负载生产环境直接执行
→ 添加maxTimeMS保护:javascriptdb.collection.explain("executionStats").find({ ... }).maxTimeMS(500);
三、executionStats核心指标深度解析
3.1 概览层关键指标
json
{
"executionTimeMillis": 28,
"nReturned": 15,
"executionStages": { ... },
"allPlansExecution": [ ... ]
}
| 指标 | 含义 | 健康值 | 问题信号 |
|---|---|---|---|
executionTimeMillis |
总执行时间(ms) | < 100ms | > 500ms |
nReturned |
返回的文档数 | 匹配业务预期 | 远小于扫描数 |
totalKeysExamined |
检查的索引条目总数 | ≈ nReturned | >> nReturned |
totalDocsExamined |
检查的文档总数 | ≈ nReturned | >> nReturned |
executionStages |
执行阶段树(核心诊断数据) | 无COLLSCAN | 出现COLLSCAN |
黄金法则 :
totalKeysExamined / nReturned < 5且totalDocsExamined / nReturned < 2
否则必然存在性能问题。
3.2 执行阶段树分析(executionStages)
执行计划以树状结构呈现,每个节点代表一个操作。关键节点类型:
| Stage类型 | 含义 | 性能信号 | 优化方向 |
|---|---|---|---|
| COLLSCAN | 全表扫描 | 严重问题 | 必须创建索引 |
| IXSCAN | 索引扫描 | 健康状态 | 检查索引选择性 |
| FETCH | 通过索引获取文档 | 正常步骤 | 优化为覆盖索引 |
| SORT | 内存排序 | 可接受 | 添加排序索引 |
| SORT stage by index | 索引排序 | 理想状态 | 无需优化 |
| SHARD_MERGE | 分片合并 | 分片集群特有 | 检查分片键设计 |
典型执行计划示例:
json
"executionStages": {
"stage": "FETCH",
"nReturned": 15,
"executionTimeMillisEstimate": 25,
"docsExamined": 15,
"inputStage": {
"stage": "IXSCAN",
"indexName": "status_1_user_id_1",
"keysExamined": 15,
"indexBounds": { ... }
}
}
- 健康信号 :
keysExamined = nReturned = 15- 无
COLLSCAN或SORT
3.3 索引边界分析(indexBounds)
json
"indexBounds": {
"status": ["[\"active\", \"active\"]"],
"user_id": ["[1000, 1000]"]
}
- 等值查询 :
["[value, value]"] - 范围查询 :
["[min, max]"] - 问题信号 :
- 空边界:索引未使用
["[MinKey, MaxKey]"]:索引前缀未命中
诊断技巧 :
若user_id边界为["[MinKey, MaxKey]"],说明查询条件未使用user_id,可能索引顺序错误。
四、性能问题诊断:4大类常见问题与解决方案
4.1 全表扫描问题(COLLSCAN)
症状:
json
"executionStages": {
"stage": "COLLSCAN",
"filter": { "status": "active" },
"docsExamined": 120000
}
- 根本原因 :
- 缺少必要索引
- 查询条件未匹配索引前缀
- 索引被拒绝(统计信息过期)
解决方案:
-
创建缺失索引:
javascriptdb.collection.createIndex({ status: 1 }); -
验证索引选择:
javascriptdb.collection.getIndexes(); -
刷新统计信息:
javascriptdb.collection.aggregate([{ $indexStats: {} }]);
4.2 索引效率低下问题
症状:
json
"executionStages": {
"stage": "FETCH",
"nReturned": 25,
"keysExamined": 120000,
"docsExamined": 120000
}
- 指标分析 :
keysExamined / nReturned = 4,800→ 极低的索引选择性
解决方案:
-
创建复合索引:
javascript// 将 {status:1} 改为 {status:1, created_at:-1} db.collection.createIndex({ status: 1, created_at: -1 }); -
使用
hint()强制索引(临时方案):javascriptdb.collection.find({ status: "active" }) .hint("status_1_created_at_-1");
4.3 内存排序问题(SORT)
症状:
json
"executionStages": {
"stage": "SORT",
"sortPattern": { "created_at": -1 },
"memUsage": 150000, // 字节数
"memLimit": 104857600 // 100MB
}
- 问题判定 :
memUsage > 0且 未出现SORT stage by index
解决方案:
-
添加排序索引:
javascriptdb.collection.createIndex({ status: 1, created_at: -1 }); -
限制排序数据量:
javascriptdb.collection.find({ status: "active" }) .sort({ created_at: -1 }) .limit(1000); // 避免大数据排序
4.4 分片集群特殊问题
症状:
json
"executionStages": {
"stage": "SHARD_MERGE",
"nReturned": 15,
"totalChildMillis": 1200
}
- 关键指标 :
totalChildMillis:子分片执行总时间- 高值表示分片数据分布不均
解决方案:
-
检查分片键选择:
javascriptsh.status(); // 查看chunk分布 -
重新均衡数据:
javascriptsh.enableAutoSplit(); sh.splitAt("collection", { shardKey: "new_split_point" });
五、高级诊断技巧:快速定位瓶颈
5.1 索引选择性计算
javascript
function calculateSelectivity(indexName) {
const stats = db.collection.aggregate([
{ $indexStats: {} },
{ $match: { name: indexName } }
]).next();
const totalDocs = db.collection.countDocuments();
return stats.accesses.ops / totalDocs;
}
// 选择性>0.1为有效索引
calculateSelectivity("status_1_user_id_1");
- 健康值:>0.1(每10个查询命中1次)
- 无效索引:<0.01 → 应删除
5.2 多计划对比分析
javascript
// 获取所有候选计划
const plans = db.collection.explain("allPlansExecution")
.find({ status: "active" });
// 分析各计划性能
plans.allPlansExecution.forEach(plan => {
print(`Plan ${plan.queryPlan.planId}:`);
print(` Execution time: ${plan.executionTimeMillis}ms`);
print(` Docs examined: ${plan.totalDocsExamined}`);
});
- 诊断价值 :
对比被拒绝计划与最终计划的指标,确定优化方向。
5.3 内存使用监控
javascript
// 检查排序内存使用
db.serverStatus().metrics.queryExecutor.sorts;
- 关键指标 :
sorts:总排序次数
sorts.memoryUse:内存排序次数
sorts.time:排序总耗时
危险信号 :
sorts.memoryUse / sorts > 0.5→ 超50%排序使用内存,需优化
六、优化验证:确保改进有效
6.1 优化前/后对比
javascript
// 优化前
const before = db.collection.explain("executionStats")
.find({ status: "active" });
// 创建索引...
db.collection.createIndex({ status: 1, created_at: -1 });
// 优化后
const after = db.collection.explain("executionStats")
.find({ status: "active" });
// 关键指标对比
print(`Execution time: ${before.executionTimeMillis}ms → ${after.executionTimeMillis}ms`);
print(`Keys examined: ${before.totalKeysExamined} → ${after.totalKeysExamined}`);
6.2 持续监控
javascript
// 设置监控脚本(每小时运行)
db.setProfilingLevel(1, { slowms: 50 }); // 记录>50ms查询
// 分析profile数据
db.system.profile.find({
"command.explain": { $exists: true }
});
- 告警阈值 :
executionTimeMillis > 500mstotalKeysExamined / nReturned > 10memUsage > 50MB
七、避坑指南:5大致命错误
错误1:仅看executionTimeMillis
后果 :忽略资源消耗,掩盖潜在问题
解决方案 :结合keysExamined和docsExamined综合判断
错误2:优化后未验证执行计划
后果 :索引未被使用,性能无提升
解决方案 :每次优化后必须重新运行explain()
错误3:盲目添加索引
后果 :索引膨胀,写入性能下降
解决方案 :用calculateSelectivity()评估索引价值
错误4:分片集群忽略SHARD_MERGE
后果 :误判单分片性能,忽略全局瓶颈
解决方案 :始终检查totalChildMillis
错误5:未处理索引碎片
后果 :索引选择性下降,性能逐步恶化
解决方案:定期重建索引:
javascript
db.collection.reIndex();
八、终极诊断流程:从问题到解决
8.1 问题确认
- 通过
system.profile定位慢查询 - 提取查询语句和参数
8.2 执行计划获取
javascript
db.collection.explain("executionStats")
.find(query)
.maxTimeMS(500);
8.3 瓶颈分析
- 检查
stage类型:是否存在COLLSCAN? - 计算比值:
keysExamined / nReturned - 验证索引:
indexBounds是否合理?
8.4 优化实施
- 创建/调整索引
- 重写查询语句
- 调整分片策略(分片集群)
8.5 效果验证
- 对比优化前后的
executionStats - 监控生产环境指标7天
九、总结:执行计划分析的黄金法则
"nReturned必须接近keysExamined,排序必须由索引完成"
核心原则:
- 索引效率 :
keysExamined / nReturned ≤ 5 - 无内存排序 :确保
SORT stage by index出现 - 无全表扫描 :杜绝
COLLSCAN - 持续监控:定期检查执行计划变化
性能目标:
| 指标 | 健康值 | 问题值 |
|---|---|---|
executionTimeMillis |
< 100ms | > 500ms |
keysExamined / nReturned |
≤ 5 | > 10 |
docsExamined / nReturned |
≤ 2 | > 5 |
| 内存排序比例 | < 5% | > 20% |
立即行动:
- 运行
db.currentOp({ "secs_running": { $gt: 1 } })查找慢查询 - 对每个慢查询执行
explain("executionStats") - 按本文方法优化,90%的查询可在1小时内提升性能50%+
通过系统化的执行计划分析,您将从"猜索引"进入"精准优化"时代。记住:最好的索引是能完全覆盖查询的索引 ,而executionStats是验证这一点的唯一标准。