Skip to content

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 ​

使用方法:

  1. 打开 MongoDB Compass
  2. 连接到 MongoDB 实例
  3. 导航到 "Collections" 标签页
  4. 选择集合,点击 "Filter" 按钮
  5. 输入查询条件,点击 "Explain Plan" 按钮
  6. 查看执行计划和统计信息

2. mongosh ​

使用方法:

  1. 连接到 MongoDB
  2. 使用 explain() 方法分析查询计划
  3. 查看输出结果,分析执行计划

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 阶段,使用索引排序