bigywater.tech
MongoDB · 34

计划缓存:什么时候会失效重选

计划缓存什么时候建立、什么时候失效重选?这篇用 $planCacheStats 看条目的生命周期,构造一个数据倾斜的查询触发 replan,再验证索引变化和手动清理。

inactive → active
缓存条目第 1 次执行后是 inactive,第 2 次执行同一形状才变 active
11 → 110
触发重选的 works:预期 11 次,实际至少 110 次(10 倍)
3 → 0
创建一个新索引后该集合的计划缓存条目数

条目的生命周期

实测 · 本机执行输出
  test> planCacheStats(刚建好索引,还没查过)
  []
  test> 第 1 次:db.t.find({a: 0, b: 2})
  2
  test> planCacheStats
  [
    {
      isActive: false,
      works: Long('11'),
      plan: 'FETCH > IXSCAN(a_1)',
      createdFrom: '{"a":0,"b":2}'
    }
  ]
  test> 第 2 次:同一个形状
  2
  test> planCacheStats
  [
    {
      isActive: true,
      works: Long('11'),
      plan: 'FETCH > IXSCAN(a_1)',
      createdFrom: '{"a":0,"b":2}'
    }
  ]

第一次执行产生一个 isActive: false 的条目,记录计划与创建时的 works;同一形状第二次执行后才变成 active。

按形状缓存

实测 · 本机执行输出
  test> db.t.find({a: 0, b: 4}).toArray().length; db.t.find({a: 0, b: 3}).toArray().length
  2
  test> 缓存条目数(同形状不同常量)
  1
  test> db.t.find({a: 0, b: 2, c: 7}).toArray().length; db.t.find({a: 0, b: 2}).sort({c: 1}).toArray().length
  2
  test> 缓存条目数(加了 c 条件、加了 sort 之后)
  3

缓存按查询形状存放,常量不同共用一个条目;加条件或加排序是新的形状,各有各的条目。

是否来自缓存

实测 · 本机执行输出
  test> db.t.find({a: 0, b: 1}) 再执行一次后 explain().queryPlanner.winningPlan.isCached
  true

explain 的 winningPlan.isCached 为 true 表示计划来自缓存。

重选

实测 · 本机执行输出
  test> db.setProfilingLevel(2)
  {
    was: 0,
    slowms: 100,
    slowinprogms: Long('5000'),
    sampleRate: 1,
    ok: 1
  }
  test> db.t.find({a: 1, b: 2})    // a=1 占 99.99% 的文档
  19998
  test> db.system.profile.find({"command.filter": {a: 1, b: 2}, op: "query"}, {_id: 0, replanned: 1, replanReason: 1, planSummary: 1, keysExamined: 1, docsExamined: 1, planCacheShapeHash: 1}).toArray()
  [
    {
      keysExamined: 103,
      docsExamined: 103,
      replanned: true,
      replanReason: 'cached plan was less efficient than expected: expected trial execution to take 11 works but it took at least 110 works',
      planCacheShapeHash: 'D1F2A12D',
      planSummary: 'IXSCAN { b: 1 }'
    }
  ]
  test> db.setProfilingLevel(0)
  {
    was: 2,
    slowms: 100,
    slowinprogms: Long('5000'),
    sampleRate: 1,
    ok: 1
  }
  test> planCacheStats(重选之后条目里的计划)
  [
    {
      isActive: false,
      works: Long('22'),
      plan: 'FETCH > IXSCAN(b_1)',
      createdFrom: '{"a":1,"b":2}'
    },
    {
      isActive: false,
      works: Long('14'),
      plan: 'SORT > FETCH > IXSCAN(a_1)',
      createdFrom: '{"a":0,"b":2}'
    },
    {
      isActive: false,
      works: Long('11'),
      plan: 'FETCH > IXSCAN(a_1)',
      createdFrom: '{"a":0,"b":2,"c":7}'
    }
  ]

缓存的计划在新常量上比预期慢很多(works 超过缓存时的 10 倍)时会重选:profiler 记录 replanned: true 和原因,条目被换成新计划。

失效

实测 · 本机执行输出
  test> 缓存条目数(之前)
  3
  test> db.t.createIndex({c: 1, b: 1})
  c_1_b_1
  test> 缓存条目数(创建索引 c_1_b_1 之后)
  0
  test> 重新执行两次后
  1
  test> hideIndex 之后的缓存条目数
  0

创建索引、隐藏索引会清空该集合的缓存条目;getPlanCache().clear() 可以手动清空。

条目里有什么

实测 · 本机执行输出
  test> 一个缓存条目的字段
  {
    keys: [
      'version',            'queryHash',
      'planCacheShapeHash', 'planCacheKey',
      'isActive',           'works',
      'reads',              'timeOfCreation',
      'createdFromQuery',   'cachedPlan',
      'creationExecStats',  'candidatePlanScores',
      'indexFilterSet',     'estimatedSizeBytes',
      'solutionHash',       'host'
    ],
    candidatePlanScores: [ 2.182018181818182, 1.1820181818181819 ],
    creationWorks: [ 11, 11 ]
  }
  test> db.t.getPlanCache().clear() 之后的条目数
  0

除了计划本身,还有创建时的执行统计、候选计划的分数、查询哈希、估计大小等。

分步演示

计划缓存
本步的对象

    本文用到的工具与边界

    • 本机 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、时间戳等每次运行都不同。缓存条目来自 $planCacheStats;重选通过 profiler(级别 2)的 replanned 与 replanReason 观察,profiler 里的 keysExamined 只对应第一批结果
    • 没有验证:internalQueryCacheEvictionRatio 与 internalQueryCacheWorksGrowthCoefficient 对重选阈值的精确作用;缓存条目数上限与淘汰;副本集 secondary 上的计划缓存是否独立;重启、dropIndex、collMod 对缓存的影响;SBE 引擎的计划缓存;索引过滤器(indexFilter)的作用
    京ICP备2026067502号-1京公网安备11010502063487号