🔥关注墨瑾轩,带你探索编程的奥秘!🚀
🔥超萌技术攻略,轻松晋级编程高手🚀
🔥技术宝库已备好,就等你来挖掘🚀
🔥订阅墨瑾轩,智趣学习不孤单🚀
🔥即刻启航,编程之旅更有趣🚀

在这里插入图片描述在这里插入图片描述

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表示升序。

为什么这个指标重要?

  • 确认是否使用了索引:如果cursorBasicCursor,表示没有使用索引,进行了全集合扫描。
  • 查看索引名称:通过索引名称可以查看系统索引信息。
2. n: 返回文档数量
"n": 1,

解读:

  • 返回的文档数量。

为什么这个指标重要?

  • 与nscanned对比:如果n很小,但nscanned很大,说明查询扫描了大量文档,但只返回少量,可能索引不够高效。
3. nscanned: 扫描文档数量
"nscanned": 100,

解读:

  • 查询扫描的文档数量。

为什么这个指标重要?

  • 衡量索引效率nscanned越小越好,理想情况是等于n
  • 发现索引问题:如果nscanned远大于n,说明索引不够高效。
4. indexOnly: 是否使用了索引覆盖
"indexOnly": true,

解读:

  • true表示查询只使用了索引,没有回表查询文档。

为什么这个指标重要?

  • 判断索引覆盖:如果indexOnlytrue,表示索引覆盖了查询所需的所有字段,不需要回表查询文档,性能最好。
  • 优化索引:如果indexOnlyfalse,说明需要回表查询文档,可能需要创建覆盖索引。
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": {}
}

问题分析:

  • cursorBasicCursor,表示没有使用索引,进行了全集合扫描。
  • 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"
      ]
    ]
  }
}

优化后分析:

  • cursorBtreeCursor,表示使用了索引。
  • nscanned是50,n是50,说明扫描了50个文档,返回50个,索引效率高。
  • indexOnlytrue,表示查询只使用了索引,没有回表查询文档。
  • 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分析查询,找出性能瓶颈。
  • 动态调整,而不是静态配置:根据查询动态优化索引。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐