bigywater.tech
MongoDB · 26

explain 的三种模式:同一条查询看到的信息有什么不同

explain 的 queryPlanner、executionStats、allPlansExecution 有什么区别?哪一种会真的执行?这篇用 10 万个文档和三个候选索引,在同一条查询上把三种模式的输出逐个拆开,再验证 update / delete / aggregate 的 explain。

3 个候选
同一条查询有 a_1、b_1、a_1_b_1 三个候选计划,allPlansExecution 里各试跑了 101 次 works
500 / 500
executionStats 里这条查询检查的键数 / 文档数,等于返回的 500 条
不修改数据
对 update 和 delete 做 explain 之后,文档数仍是 100000

queryPlanner

实测 · 本机执行输出
  test> db.t.find({a: 5, b: {$gte: 500}}).explain("queryPlanner")
  {
    keys: [
      'explainVersion',
      'queryPlanner',
      'queryShapeHash',
      'command',
      'serverInfo',
      'serverParameters',
      'ok'
    ],
    plan: 'FETCH > IXSCAN(a_1_b_1)',
    rejected: [ 'FETCH > IXSCAN(b_1)', 'FETCH > IXSCAN(a_1)' ],
    hasExecutionStats: false
  }

默认模式只做计划选择,不执行查询,输出里没有 executionStats;能看到赢家(这里是复合索引 a_1_b_1)和落选的计划。

executionStats

实测 · 本机执行输出
  test> db.t.find({a: 5, b: {$gte: 500}}).explain("executionStats")
  {
    plan: 'FETCH > IXSCAN(a_1_b_1)',
    nReturned: 500,
    keysExamined: 500,
    docsExamined: 500,
    ms: 1
  }
  test>   executionStats.executionStages 沿 inputStage 往下展开
  [
    { stage: 'FETCH', nReturned: 500, works: 501, advanced: 500 },
    { stage: 'IXSCAN', nReturned: 500, works: 501, advanced: 500 }
  ]

真的执行查询,给出 nReturned、totalKeysExamined、totalDocsExamined 和每个阶段的 works / advanced。

allPlansExecution

实测 · 本机执行输出
  test> db.t.find({a: 5, b: {$gte: 500}}).explain("allPlansExecution")
  {
    winner: 'classic',
    candidates: [
      {
        nReturned: 101,
        keysExamined: 101,
        docsExamined: 101,
        works: 101
      },
      { nReturned: 0, keysExamined: 101, docsExamined: 101, works: 101 },
      { nReturned: 50, keysExamined: 101, docsExamined: 101, works: 101 }
    ]
  }

在 executionStats 之上多出 allPlansExecution:每个候选计划试跑的统计,可以看到赢家在同样的 works 里返回得最多。

写操作的 explain

实测 · 本机执行输出
  test> db.t.countDocuments({})
  100000
  test> var e = db.runCommand({explain: {update: "t", updates: [{q: {a: 5, b: 505}, u: {$set: {z: 1}}, multi: true}]}, verbosity: "executionStats"}); ({stage: e.executionStats.executionStages.stage, nWouldModify: e.executionStats.executionStages.nWouldModify, docsExamined: e.executionStats.totalDocsExamined})
  { stage: 'UPDATE', nWouldModify: 100, docsExamined: 100 }
  test> var e = db.runCommand({explain: {delete: "t", deletes: [{q: {a: 5}, limit: 0}]}, verbosity: "executionStats"}); ({stage: e.executionStats.executionStages.stage, nWouldDelete: e.executionStats.executionStages.nWouldDelete, docsExamined: e.executionStats.totalDocsExamined})
  { stage: 'BATCHED_DELETE', nWouldDelete: 1000, docsExamined: 1000 }
  test> db.t.countDocuments({}) + " 个文档;有 z 字段的 " + db.t.countDocuments({z: {$exists: true}}) + " 个"
  100000 个文档;有 z 字段的 0 个

对 update、delete 做 explain 只给出会修改 / 删除多少文档(nWouldModify、nWouldDelete),数据不变。

aggregate

实测 · 本机执行输出
  test> db.t.explain("executionStats").aggregate([{$match: {a: 5, b: {$gte: 500}}}, {$group: {_id: "$a", n: {$sum: 1}}}])
  {
    stages: '(没有 stages 字段)',
    plan: 'GROUP > PROJECTION_COVERED > IXSCAN(a_1_b_1)',
    docsExamined: 0
  }

aggregate 的 explain 里 $match 与 $group 可以一起下推,整个 pipeline 成为一个计划;字段都在索引里时还是覆盖查询。

三种模式的差别

实测 · 本机执行输出
  # 比较三种模式的输出里有哪些键
  test> var b = Object.keys(db.t.find({a: 5, b: {$gte: 500}}).explain("executionStats").executionStats), d = Object.keys(db.t.find({a: 5, b: {$gte: 500}}).explain(true).executionStats); ({queryPlanner_has_executionStats: Object.keys(db.t.find({a: 5, b: {$gte: 500}}).explain("queryPlanner")).includes("executionStats"), executionStats_keys: b, allPlans_extra: d.filter(k => !b.includes(k))})
  {
    queryPlanner_has_executionStats: false,
    executionStats_keys: [
      'executionSuccess',
      'nReturned',
      'executionTimeMillis',
      'executionTimeMicros',
      'totalKeysExamined',
      'totalDocsExamined',
      'executionStages'
    ],
    allPlans_extra: [ 'allPlansExecution' ]
  }

queryPlanner < executionStats < allPlansExecution,信息量逐级增加;explain(true) 等价于 allPlansExecution。

分步演示

explain 三种模式
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MongoDB 9.0.2(Homebrew 的 mongod、mongos)/ mongosh 2.12.0 / pymongo 4.18.3;每个实验在临时目录里起全新的 mongod(独立 dbpath,端口 9900 以上,只监听 127.0.0.1,WiredTiger 缓存 0.25 GB),需要副本集或分片集群时在本机起多个进程;命令输出来自真实的 mongosh,批量写入、计时与多线程用 pymongo;计时用 time.perf_counter,数字每次运行会有小幅抖动,ObjectId、时间戳等每次运行都不同。数据是 10 万个文档(a = i % 100,b = i % 1000),候选索引 a_1、b_1、a_1_b_1;所有输出来自 mongosh 的 explain
    • 没有验证:分片集群上的 explain 输出(shards 字段);explain 里 slot-based 执行(SBE)与经典引擎的字段差异(这里的 find 走经典引擎,aggregate 下推后的 queryPlan 层级不同);executionTimeMillis 在慢查询上的精度;explain 对计划缓存的影响;queryShapeHash 的用途
    京ICP备2026067502号-1京公网安备11010502063487号