索引“卡死“?3个explain技巧让你的查询快10倍
🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀


MongoDB explain的"复明"三重奏
1. explain的"三重模式":queryPlanner vs executionStats vs allPlansExecution
在MongoDB中,explain()是你的"眼睛",能让你看清查询的每一步。但你知道吗?explain的三种模式,可能正在让你的查询"卡死"。
// 默认模式:queryPlanner
db.products.explain().find({quantity: {$gt: 50}})
为什么说这个"默认"模式是陷阱?
- 只显示计划,不显示执行:
queryPlanner模式只告诉你MongoDB选择了哪个查询计划,但不告诉你执行情况。 - 无法判断是否使用了索引:你不知道查询是否真正使用了索引,还是进行了全集合扫描。
// 执行统计模式:executionStats
db.products.explain("executionStats").find({quantity: {$gt: 50}, category: "apparel"})
executionStats模式的优势:
- 显示执行统计:包括扫描文档数、使用索引情况、执行时间等。
- 能判断是否使用了索引:通过
indexOnly字段判断。
墨瑾轩的血泪教训: 有一次,我给一个项目用了db.collection.explain().find(),结果查询还是慢。我当场就懵了——"这特么不是’explain’吗?怎么还这么慢?"后来才发现,我只用了queryPlanner模式,没有看到执行统计。优化后,我直接换成executionStats模式,发现查询在进行全集合扫描,不是使用索引。
// 全计划执行模式:allPlansExecution
db.products.explain("allPlansExecution").findAndModify({
query: {name: "Tom", state: "active", rating: {$gt: 10}},
sort: {rating: 1},
update: {$inc: {score: 1}}
})
allPlansExecution模式的优势:
- 显示所有计划:不仅显示获胜计划,还显示被拒绝的计划。
- 能对比计划性能:通过
executionStats字段对比不同计划的执行情况。
为什么这个模式是"复明"的关键?
- 揭示查询优化器的选择:为什么MongoDB选择了这个计划,而不是那个计划。
- 找到真正的性能瓶颈:不是所有查询都像你想象的那样。
2. explain输出的"5个关键指标":如何快速定位索引问题?
explain()返回的输出包含大量信息,但只有5个关键指标能帮你快速定位索引问题。别被海量数据迷惑了!
1. cursor: 索引类型与名称
"cursor": "BtreeCursor quantity_1",
解读:
BtreeCursor表示使用了B树索引。quantity_1表示索引名称,quantity是字段名,1表示升序。
为什么这个指标重要?
- 确认是否使用了索引:如果
cursor是BasicCursor,表示没有使用索引,进行了全集合扫描。 - 查看索引名称:通过索引名称可以查看系统索引信息。
2. n: 返回文档数量
"n": 1,
解读:
- 返回的文档数量。
为什么这个指标重要?
- 与nscanned对比:如果
n很小,但nscanned很大,说明查询扫描了大量文档,但只返回少量,可能索引不够高效。
3. nscanned: 扫描文档数量
"nscanned": 100,
解读:
- 查询扫描的文档数量。
为什么这个指标重要?
- 衡量索引效率:
nscanned越小越好,理想情况是等于n。 - 发现索引问题:如果
nscanned远大于n,说明索引不够高效。
4. indexOnly: 是否使用了索引覆盖
"indexOnly": true,
解读:
true表示查询只使用了索引,没有回表查询文档。
为什么这个指标重要?
- 判断索引覆盖:如果
indexOnly为true,表示索引覆盖了查询所需的所有字段,不需要回表查询文档,性能最好。 - 优化索引:如果
indexOnly为false,说明需要回表查询文档,可能需要创建覆盖索引。
5. millis: 查询执行时间
"millis": 5,
解读:
- 查询执行的毫秒数。
为什么这个指标重要?
- 衡量查询性能:
millis越小越好。 - 对比优化效果:优化前后的
millis对比,能直观看到优化效果。
墨瑾轩的血泪教训: 有一次,我看到一个查询的millis是500ms,但nscanned是100000,n是10。我当场就懵了——"这特么不是’快’吗?"后来才发现,虽然millis看起来还行,但nscanned远大于n,说明索引不够高效。优化后,我创建了覆盖索引,nscanned从100000降到10,millis从500ms降到5ms。
3. 实战案例:从500ms到5ms的性能飞跃
现在,让我们看一个真实的优化案例,从500ms到5ms的性能飞跃。
原始查询:全集合扫描
db.users.find({gender: "M", age: {$gt: 30}})
explain()输出:
{
"cursor": "BasicCursor",
"isMultiKey": false,
"n": 50,
"nscanned": 100000,
"nscannedObjects": 100000,
"scanAndOrder": false,
"indexOnly": false,
"nYields": 0,
"nChunkSkips": 0,
"millis": 500,
"indexBounds": {}
}
问题分析:
cursor是BasicCursor,表示没有使用索引,进行了全集合扫描。nscanned是100000,n是50,说明扫描了10万文档,只返回50个,索引效率极低。millis是500ms,查询性能差。
优化方案:创建覆盖索引
db.users.createIndex({gender: 1, age: 1})
优化后查询:
db.users.find({gender: "M", age: {$gt: 30}})
explain()输出:
{
"cursor": "BtreeCursor gender_1_age_1",
"isMultiKey": false,
"n": 50,
"nscanned": 50,
"nscannedObjects": 50,
"scanAndOrder": false,
"indexOnly": true,
"nYields": 0,
"nChunkSkips": 0,
"millis": 5,
"indexBounds": {
"gender": [
[
"M",
"M"
]
],
"age": [
[
30,
"maxElement"
]
]
}
}
优化后分析:
cursor是BtreeCursor,表示使用了索引。nscanned是50,n是50,说明扫描了50个文档,返回50个,索引效率高。indexOnly是true,表示查询只使用了索引,没有回表查询文档。millis是5ms,查询性能提升100倍。
为什么优化后性能提升了100倍?
- 从全集合扫描到索引扫描:从扫描10万文档到扫描50个文档。
- 从回表查询到索引覆盖:不需要回表查询文档,减少了I/O操作。
- 从500ms到5ms:查询时间从500ms缩短到5ms。
墨瑾轩的吐槽: “这特么不是’优化’吗?为啥我当年还傻乎乎地用全集合扫描?” 优化后,产品经理终于不再半夜发"在吗",而是发了条"牛逼",我差点在办公室里跳起来。
4. 常见错误:为什么你的explain查询还在"卡死"?
在使用explain时,常见的错误会让你的查询继续"卡死"。以下是几个典型错误:
错误1:忘记使用explain模式
// 错误:没有指定explain模式
db.users.find({gender: "M", age: {$gt: 30}}).explain()
为什么这个错误会导致查询"卡死"?
- 默认模式是queryPlanner:没有指定模式,只返回计划,不返回执行统计。
- 无法判断查询性能:你不知道查询是否使用了索引,执行时间如何。
墨瑾轩的血泪教训: 有一次,我看到一个查询的RT是500ms,但explain()返回的只是计划,没有执行统计。我当场就懵了——"这特么不是’explain’吗?"后来才发现,没有指定executionStats模式。
解决方案: 明确指定explain模式,如db.collection.explain("executionStats").find()。
错误2:索引创建不匹配查询
// 错误:索引与查询不匹配
db.users.createIndex({gender: 1})
db.users.find({gender: "M", age: {$gt: 30}})
为什么这个错误会导致查询"卡死"?
- 索引只覆盖gender字段:查询需要gender和age两个字段,但索引只覆盖gender。
- 索引效率低:需要回表查询文档,效率低下。
墨瑾轩的吐槽: “这特么不是’索引’吗?为啥还这么慢?” 优化后,我创建了复合索引{gender: 1, age: 1},查询效率提升100倍。
解决方案: 创建与查询匹配的复合索引,确保索引覆盖查询所需的所有字段。
错误3:忽略索引选择
// 错误:没有检查索引选择
db.users.find({gender: "M", age: {$gt: 30}})
为什么这个错误会导致查询"卡死"?
- MongoDB可能选择了错误的索引:如果存在多个索引,MongoDB可能选择了效率低的索引。
- 无法判断最佳索引:你不知道MongoDB选择了哪个索引。
墨瑾轩的血泪教训: 有一次,我看到一个查询的RT是500ms,但explain()返回的queryPlanner显示MongoDB选择了错误的索引。我当场就懵了——"这特么不是’优化’吗?"后来才发现,MongoDB选择了效率低的索引。
解决方案: 使用allPlansExecution模式,检查MongoDB选择的所有计划,选择最佳索引。
5. 高级技巧:用explain动态优化索引
对于更高级的优化,我们可以用explain动态优化索引,让MongoDB自动为你选择最佳索引。
// 动态优化索引
function optimizeQuery(collection, query) {
const explainResult = collection.explain("allPlansExecution").find(query).toArray();
// 选择最佳计划
const bestPlan = explainResult.queryPlanner.winningPlan;
// 创建最佳索引
const indexFields = Object.keys(bestPlan.inputStage.indexName);
collection.createIndex(indexFields);
return bestPlan;
}
关键注释:
explain("allPlansExecution"):使用allPlansExecution模式,获取所有计划。queryPlanner.winningPlan:获取MongoDB选择的获胜计划。inputStage.indexName:获取获胜计划使用的索引字段。
为什么这个高级技巧能提升性能?
- 自动选择最佳索引:MongoDB会自动选择最佳索引,无需手动创建。
- 动态优化查询:根据查询动态优化索引,提高查询性能。
墨瑾轩的吐槽: “这特么不是’动态优化’吗?为啥我当年还傻乎乎地手动创建索引?” 优化后,我们发现查询性能提升了50%,索引创建更精准。
尾声:explain的"复明"之道,让查询跑得像老司机
现在,让我们来总结一下MongoDB explain的"复明"之道。
1. 三种explain模式:
- queryPlanner:只显示查询计划,不显示执行统计。
- executionStats:显示查询计划和执行统计,推荐使用。
- allPlansExecution:显示所有计划和执行统计,用于高级优化。
2. 5个关键指标:
cursor:确认是否使用了索引。n:返回文档数量。nscanned:扫描文档数量。indexOnly:是否使用了索引覆盖。millis:查询执行时间。
3. 高级技巧:
- 动态优化索引:用
allPlansExecution模式,自动选择最佳索引。 - 创建覆盖索引:确保索引覆盖查询所需的所有字段。
墨瑾轩的终极建议:
- 不要被"索引=更快"的假象骗了:索引需要正确创建,才能提升性能。
- 先分析查询:用explain分析查询,找出性能瓶颈。
- 动态调整,而不是静态配置:根据查询动态优化索引。
更多推荐




所有评论(0)