1. 为什么需要关注MongoDB执行计划当你的MongoDB查询突然变慢时第一反应是什么加索引优化查询条件这些常规操作可能有效但更聪明的做法是直接查看查询的执行计划。就像医生看病需要先看检查报告一样执行计划就是MongoDB查询的体检报告。executionStats是explain命令输出的核心部分它详细记录了查询执行的每个环节查询使用了哪些索引或没用索引扫描了多少文档各个执行阶段耗时内存使用情况排序和返回结果的处理方式我处理过的一个真实案例一个原本运行良好的聚合查询突然从200ms飙升到15秒。通过executionStats分析发现数据量增长导致原本高效的索引变得低效MongoDB转而使用了全表扫描。这就是为什么你需要掌握执行计划分析——它能帮你快速定位性能瓶颈的精确位置。2. 获取executionStats的完整方法在Mongo Shell中获取执行计划的标准命令是db.collection.find({query}).explain(executionStats)但实际使用中有几个关键细节需要注意2.1 不同explain模式的区别queryPlanner仅返回查询计划不执行查询executionStats执行查询并返回统计信息allPlansExecution显示所有候选计划的执行信息提示生产环境分析性能问题时优先使用executionStats因为它反映真实的查询执行情况。2.2 执行计划的查看技巧我习惯用这个格式化命令db.collection.explain(executionStats).find({query}).pretty()加上pretty()后输出会分层缩进更容易阅读复杂的执行计划。对于特别大的执行计划可以配合VSCode的JSON格式化插件查看。2.3 执行计划的时效性要注意的是执行计划会随着数据分布变化而变化。上周高效的查询计划这周可能就失效了。我建议对关键查询定期检查执行计划特别是在数据量增长超过20%时。3. executionStats核心指标深度解析一个典型的executionStats输出包含这些关键部分3.1 执行阶段(executionStages)这是最需要关注的部分展示了查询的实际执行路径。常见阶段包括COLLSCAN全集合扫描性能杀手IXSCAN索引扫描理想情况FETCH根据索引指针获取完整文档SORT内存排序高内存消耗PROJECTION字段投影案例一个包含排序的查询计划片段executionStages: { stage: SORT, nReturned: 100, executionTimeMillisEstimate: 32, works: 101, advanced: 100, sortPattern: {timestamp: -1}, memUsage: 1048576, memLimit: 33554432, inputStage: { stage: COLLSCAN, filter: {...}, nReturned: 10000, executionTimeMillisEstimate: 15 } }这个计划显示虽然最终只返回100条记录但因为排序字段没有合适索引MongoDB不得不先扫描10000条记录到内存中排序消耗了1MB内存。3.2 关键性能指标executionTimeMillis总执行时间毫秒totalKeysExamined检查的索引键数量totalDocsExamined检查的文档数量nReturned实际返回的文档数works执行的工作单元数健康查询的特征是totalDocsExamined ≈ nReturned totalKeysExamined ≤ nReturned * 2如果发现totalDocsExamined比nReturned大一个数量级这就是明显的性能问题信号。3.3 索引使用情况在winningPlan中可以看到实际使用的索引winningPlan: { stage: FETCH, inputStage: { stage: IXSCAN, keyPattern: {username: 1, status: 1}, indexName: username_1_status_1, isMultiKey: false, direction: forward } }这个例子显示查询使用了复合索引{username:1, status:1}。如果isMultiKey为true表示这是数组字段上的多键索引可能影响性能。4. 常见性能问题诊断实战4.1 全集合扫描(COLLSCAN)这是最常见的性能杀手。当你在executionStages中看到COLLSCAN时说明查询没有使用索引。解决方案为查询条件字段创建索引检查现有索引是否被正确使用可能因为类型不匹配使用hint()强制使用特定索引真实案例一个用户查询db.users.find({email:userexample.com})执行缓慢executionStats显示COLLSCAN。检查发现email字段没有索引创建索引后查询时间从1200ms降到3ms。4.2 内存排序(SORT)当排序字段没有索引时MongoDB必须在内存中排序这会导致高内存消耗memUsage指标执行时间随数据量线性增长解决方案为排序字段创建索引使用索引同时覆盖查询条件和排序限制排序数据量配合limit注意MongoDB内存排序默认限制32MB超过会报错。可以通过allowDiskUse选项允许使用磁盘但这会显著降低性能。4.3 索引覆盖不全理想情况是覆盖查询(covered query)即所有需要的字段都在索引中无需回表查询完整文档。检查指标totalDocsExamined 0 // 完美覆盖 totalDocsExamined nReturned // 需要回表优化方法创建包含所有需要字段的复合索引使用投影只返回索引字段4.4 索引失效的常见原因即使创建了索引也可能因为以下原因不被使用查询条件类型不匹配如字符串字段用数字查询使用了$not、$nin等不支持索引的操作符索引选择性太低如性别字段索引集合太小MongoDB可能认为全表扫描更快5. 高级分析技巧5.1 比较不同查询计划的性能使用allPlansExecution模式可以看到所有候选计划的执行情况db.collection.explain(allPlansExecution).find({...})输出中会包含rejectedPlans数组显示为什么其他计划没有被选择。这对理解MongoDB的查询优化器决策很有帮助。5.2 使用索引过滤器(index filters)临时强制使用或忽略某些索引// 强制使用特定索引 db.collection.find({...}).hint(index_name) // 忽略索引 db.collection.find({...}).hint({$natural:1})这在测试不同索引性能时非常有用但生产环境慎用因为数据变化可能导致强制索引不再高效。5.3 监控索引使用情况通过系统命令查看索引使用频率db.collection.aggregate([{ $indexStats: {} }])返回结果示例{ name: username_1_status_1, accesses: { ops: NumberLong(12345), since: ISODate(2023-01-01T00:00:00Z) } }ops值高的索引是热点索引而ops为0的索引可能可以考虑删除。6. 性能优化实战建议根据我处理过的上百个MongoDB性能案例总结出这些黄金法则索引不是越多越好每个索引都会增加写入开销。通常一个集合5-6个索引是合理上限。复合索引字段顺序很重要遵循ESR原则(Equality, Sort, Range)即等值查询字段在前排序字段居中范围查询字段在后。定期检查索引使用情况每月运行一次$indexStats删除三个月未使用的索引。关注索引选择性选择性不同值数量/总文档数。高于10%的选择性通常值得建索引。使用explain()验证所有写操作update和delete也可以使用explain()特别是当影响大量文档时。善用partial索引只为部分文档创建索引节省空间。例如db.users.createIndex( {status:1}, {partialFilterExpression: {status: {$exists:true}}} )监控内存排序发现memUsage接近32MB时要立即优化避免查询失败。使用explain()分析聚合管道聚合查询也可以使用explain()帮助优化复杂的$lookup和$group阶段。在实际项目中我通常会为团队建立这样的性能检查清单所有查询都必须有explain()分析报告禁止出现COLLSCAN特殊场景需审批排序操作必须有索引支持定期review索引使用情况这些实践让我们的MongoDB集群始终保持毫秒级响应即使数据量增长到TB级。记住好的性能不是偶然发生的而是通过持续监控和优化实现的。