计划缓存什么时候建立、什么时候失效重选?这篇用 $planCacheStats 看条目的生命周期,构造一个数据倾斜的查询触发 replan,再验证索引变化和手动清理。
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
trueexplain 的 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除了计划本身,还有创建时的执行统计、候选计划的分数、查询哈希、估计大小等。