ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

MongoDB distinct 命令与聚合管道中的 DISTINCT_SCAN 计划缓存机制解析

MongoDB distinct 命令与聚合管道中的 DISTINCT_SCAN 计划缓存机制解析 MongoDB distinct 命令与聚合管道中的 DISTINCT_SCAN 计划缓存机制解析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本篇技术指南以 MongoDB 开源仓库The MongoDB Database中的 Golden Test 文档 distinct_plan_cache.md 为主线结合其驱动测试 distinct_plan_cache_md.js、Golden 测试工具库 golden_test_utils.js 以及计划缓存核心实现 plan_cache.h系统讲解distinct命令与聚合管道生成的DISTINCT_SCAN计划如何进入计划缓存、如何在非活动/活动inactive/active状态间转换以及不同谓词如何复用同一缓存条目。读完本文你将理解计划缓存条目的内部结构、isActive状态机的运行机制以及 Golden Test 如何固化与校验这些查询优化行为。一、背景DISTINCT_SCAN 与计划缓存1.1 什么是 DISTINCT_SCANDISTINCT_SCAN是 MongoDB 查询执行树中的一种特殊索引扫描阶段stage它按索引顺序扫描索引键并跳过相邻的重复键值从而在返回去重结果的同时避免全量扫描与内存去重。其核心优势在于——去重逻辑被下推进索引扫描本身配合覆盖投影PROJECTION_COVERED时甚至不需要回表取文档isFetching: false。在源码中索引是否适合做DISTINCT_SCAN转换有专门判定逻辑见 distinct_access.cppCheck if an index is suitable for the DISTINCT_SCAN transition转换失败时会在 distinct_access.cpp 触发uassert(8404000, Failed to finalize a DISTINCT_SCAN plan, soln)断言。1.2 什么是计划缓存Plan Cache当一条查询存在多个候选执行计划时MongoDB 的查询规划器query planner会进行多计划竞争multi-planning选出获胜计划后将其按查询形状query shape哈希后写入计划缓存。这样后续形状相同的查询可以直接复用缓存中的获胜计划避免重复的多计划竞争开销。计划缓存条目PlanCacheEntryBase的核心字段在 plan_cache.h 中定义包括cachedPlan可用来重建完整执行计划的缓存计划planCacheShapeHash/planCacheKey查询形状哈希与计划缓存键isActive条目是否处于活动状态timeOfCreation创建时间debugInfo调试信息当累计缓存大小超过阈值后会被剥离但 SBE 的调试信息始终保留见 plan_cache.h。1.3 isActive非活动与活动计划计划缓存条目有非活动inactive与活动active两种状态非活动条目刚写入缓存、尚未被复用或表现不佳被降级时的状态参与后续的缓存竞争活动条目被后续同形状查询成功复用时进入的状态代表稳定可用的执行计划。状态转换的底层实现在 plan_cache.h 的deactivate()方法中当关联计划表现变差时会克隆条目并把isActive置为false而get()在返回缓存条目时会依据isActive区分kPresentActive与kPresentInactive两种状态见 plan_cache.h。Golden 测试正是把这一状态迁移过程固化下来同一查询第一次执行后缓存条目为isActive: false第二次执行复用后变为isActive: true。二、Golden Test 如何验证 distinct 的计划缓存行为本文讨论的 distinct_plan_cache.md 是一份Golden Test 期望输出expected output文件由驱动测试 distinct_plan_cache_md.js 生成并与实际输出比对。该测试文件头部注释明确说明了测试目的Tests DISTINCT_SCANs generated from multiplanning correctly utilize the plan cache.测试带有featureFlagShardFilteringDistinctScan与requires_fcv_82两个标签且输出文件位于featureFlagSbeFull/目录下说明该场景在SBESlot-Based Execution全量启用的引擎配置下运行。2.1 核心工具函数测试用到的两个关键校验函数定义在 golden_test_utils.js 中validateDistinctPlanCacheUse(coll, key, filter)先调用coll.getPlanCache().clear()清空计划缓存然后连续执行两次distinct命令分别输出小节 DISTINCT_SCAN stored as inactive plan第一次条目以非活动状态入缓存与 DISTINCT_SCAN used as active plan第二次条目被复用并转为活动状态见 golden_test_utils.jsvalidateAggPlanCacheUse(coll, pipeline)同样的流程但针对聚合管道见 golden_test_utils.js。每次执行后调用outputPlanCacheStats(coll)通过$planCacheStats聚合阶段读取缓存状态并只保留cachedPlan、planCacheKey、createdFromQuery、isActive、shard五个字段输出到 Markdown见 golden_test_utils.js——这就是期望输出文档中每个 JSON 块的由来。2.2 测试数据集驱动测试在多个小节中构造了不同的数据与索引组合详见 distinct_plan_cache_md.js核心特点是在x、y、z等多个字段上创建多个复合索引如{x:1,y:1}、{y:1,x:1}、{x:1,y:1,z:1}等迫使查询规划器面对多个可选索引进行多计划竞争从而产生可缓存的获胜计划。三、场景一distinct 命令利用计划缓存测试数据为 12 条包含x、y、z字段的文档索引为{x:1,y:1}、{y:1,x:1}、{x:1,z:1,y:1}。对x字段执行distinct过滤条件为{x: {$gt: 3}, y: 5}见 distinct_plan_cache_md.js。3.1 第一次执行DISTINCT_SCAN 以非活动计划入缓存首次执行后$planCacheStats输出如下节选自 distinct_plan_cache.md[ { cachedPlan : { inputStage : { direction : forward, indexBounds : { x : [(3.0, inf]], y : [[5.0, 5.0]] }, indexName : y_1_x_1, indexVersion : 2, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : false, isSparse : false, isUnique : false, keyPattern : { x : 1, y : 1 }, multiKeyPaths : { x : [ ], y : [ ] }, stage : DISTINCT_SCAN }, stage : PROJECTION_COVERED, transformBy : { } }, createdFromQuery : { distinct : { key : x }, projection : { }, query : { x : { $gt : 3 }, y : 5 }, sort : { } }, isActive : false, planCacheKey : 6513BB87 } ]解读这份缓存条目cachedPlan获胜计划是PROJECTION_COVERED覆盖投影下套一个DISTINCT_SCAN。扫描方向为forward使用y_1_x_1索引索引版本 2isFetching: false表明完全覆盖、无需回表indexBoundsx的边界(3.0, inf]对应$gt: 3开区间y的边界[5.0, 5.0]对应等值y: 5闭区间——这是把过滤条件下推到索引扫描后的区间表示createdFromQuery记录了该条目由哪个请求生成distinct.key为x、query为原始过滤条件、sort为空planCacheKey6513BB87该查询形状的哈希键后续同形状查询据此命中isActive: false首次进入缓存处于非活动状态。3.2 第二次执行DISTINCT_SCAN 作为活动计划被复用第二次执行完全相同的distinct命令后缓存条目中除isActive变为true外其余字段完全一致见 distinct_plan_cache.md。这说明第二次查询命中了同一个planCacheKey6513BB87直接复用了缓存计划没有重新进行多计划竞争命中后条目被标记为活动代表该计划通过了复用验证成为稳定的默认选择。四、场景二不同谓词复用同一计划缓存条目场景二插入 100 条{x: i % 2, y: i 100, z: i 200}文档并创建{x:1}、{x:1,y:1}、{y:1,z:1}、{x:1,y:1,z:1}四个索引见 distinct_plan_cache_md.js。随后连续执行两个谓词不同的 distinctdistinct(x, {x: {$gt: 12}, y: {$lt: 200}})distinct(x, {x: {$gt: 12}, y: {$lt: 250}})仅把y的上界从 200 改为 250两次执行对应的缓存输出见 distinct_plan_cache.md第二次的完整输出如下[ { cachedPlan : { inputStage : { direction : forward, indexBounds : { x : [(12.0, inf]], y : [[-inf, 250.0)] }, indexName : x_1_y_1, indexVersion : 2, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : false, isSparse : false, isUnique : false, keyPattern : { x : 1, y : 1 }, multiKeyPaths : { x : [ ], y : [ ] }, stage : DISTINCT_SCAN }, stage : PROJECTION_COVERED, transformBy : { } }, createdFromQuery : { distinct : { key : x }, projection : { }, query : { x : { $gt : 12 }, y : { $lt : 250 } }, sort : { } }, isActive : true, planCacheKey : 3B5005C1 } ]关键观察两次查询的planCacheKey都是3B5005C1说明计划缓存按查询形状而非具体字面值寻址——y: {$lt: 200}与y: {$lt: 250}属于同一形状因此第二次直接命中第一个查询建立的缓存条目。变化只体现在两处indexBounds中y的边界从[-inf, 200.0)更新为[-inf, 250.0)缓存计划是形状模板具体区间在复用时按当前谓词重新计算isActive从false变为true非活动条目被复用后转为活动。值得注意的是获胜索引从场景一的y_1_x_1变成了这里的x_1_y_1——这取决于当前数据集下各候选计划的代价排序也再次说明计划缓存固化的是规划器当时的择优结果。五、场景三集合无重复值时优先缓存 IXSCAN 而非 DISTINCT_SCAN场景三插入 100 条{x: i, y: i 100, z: i 200}文档其中x从 0 到 99 完全无重复索引为{x:1}、{x:1,y:1}、{y:1,z:1}见 distinct_plan_cache_md.js。对x执行 distinct条件为{x: {$gt: -1}, y: {$lt: 105}}输出见 distinct_plan_cache.md。关键发现这个场景下缓存下来的获胜计划不是DISTINCT_SCAN而是普通的IXSCAN FETCH{ cachedPlan : { filter : { x : { $gt : -1 } }, inputStage : { direction : forward, indexBounds : { y : [[-inf, 105.0)], z : [[MinKey, MaxKey]] }, indexName : y_1_z_1, indexVersion : 2, isMultiKey : false, isPartial : false, isSparse : false, isUnique : false, keyPattern : { y : 1, z : 1 }, multiKeyPaths : { y : [ ], z : [ ] }, stage : IXSCAN }, stage : FETCH }, createdFromQuery : { distinct : { key : x }, projection : { }, query : { x : { $gt : -1 }, y : { $lt : 105 } }, sort : { } }, isActive : false, planCacheKey : E105DA4A }5.1 为什么规划器不选 DISTINCT_SCAN这背后是代价模型的理性选择当集合中x字段没有重复值时DISTINCT_SCAN与普通IXSCAN扫描的行数几乎相同DISTINCT_SCAN的跳过重复值优势完全无法体现而此时y_1_z_1索引上的IXSCAN还能通过y的范围条件[-inf, 105.0)高效裁剪扫描区间因此普通索引扫描的总体代价更低。同时注意缓存计划中的filter: {x: {$gt: -1}}由于x不在扫描索引y_1_z_1中$gt: -1无法下推为索引边界只能作为FETCH之后的残留过滤条件保留。5.2 印证与启发这与源码中DISTINCT_SCAN的适用性判断逻辑一致该优化仅在去重能显著减少扫描量时才有价值。Golden Test 固化这一场景正是为了防止未来优化器改动导致这类行为回归——例如错误地强制使用DISTINCT_SCAN而牺牲掉更优的普通索引扫描。测试名称 Prefer cached IXSCAN over DISTINCT_SCAN for no duplicate values in the collection 直接点明了这一预期。六、场景四聚合管道的 DISTINCT_SCAN 同样走计划缓存场景四将验证对象从distinct命令扩展到聚合管道。测试创建 10 个复合索引插入含a、b、c、d字段的文档其中包含数组字段d: [1,2,3]以测试多键场景详见 distinct_plan_cache_md.js。6.1 管道一$sort $group($first)[ { $sort : { a : 1, b : 1 } }, { $group : { _id : $a, accum : { $first : $b } } } ]该管道等价于按a分组、每组取b的第一个值由于$sort已按(a, b)排序每组第一条记录的b即为$first的结果。缓存条目见 distinct_plan_cache.md显示执行树为PROJECTION_COVERED下的DISTINCT_SCAN索引为a_1_b_1isFetching: falseindexBounds中a、b均为[MinKey, MaxKey]全区间扫描管道无过滤条件createdFromQuery中distinct.key为aprojection为{_id: 0, a: 1, b: 1}sort为{a: 1, b: 1}——说明聚合管道在执行前被重写为等价的 distinct projection sort 形式参与规划与缓存与命令形式一致第一次执行isActive: false第二次复用后isActive: trueplanCacheKey保持AF5172A0不变。6.2 管道二$group $bottom[ { $group : { _id : $a, accum : { $bottom : { sortBy : { a : -1, b : -1 }, output : $c } } } } ]$bottom需要按(a: -1, b: -1)排序取最后一条记录的c。缓存条目见 distinct_plan_cache.md显示索引扩展为三键a_1_b_1_c_1indexBounds中a、b、c均为[MinKey, MaxKey]projection相应扩展为{_id: 0, a: 1, b: 1, c: 1}同样经历了isActive: false → true的状态迁移planCacheKey为4DDFB1B7。这证明管道重写生成的 distinct 计划与命令生成的 distinct 计划走的是同一条计划缓存通路且投影字段集合、排序键会作为查询形状的一部分影响缓存键。七、场景五带内嵌 FETCH 的 DISTINCT_SCAN 利用计划缓存场景五延续场景四的数据集验证$top/$bottom这两种需要回表取值的聚合见 distinct_plan_cache_md.js。7.1 $top 场景[ { $group : { _id : $a, accum : { $top : { sortBy : { a : 1, b : 1 }, output : $c } } } } ]缓存条目见 distinct_plan_cache.md与场景四的$bottom结构类似使用a_1_b_1_c_1索引isActive从false变为trueplanCacheKey为07D4BDAB。7.2 内嵌 FETCH体现在哪里小节标题中的 embedded FETCH 指的是当DISTINCT_SCAN需要取出不在索引中的字段此处是$top/$bottom的output: $c虽在索引中但多键、覆盖等场景下仍需取文档时FETCH阶段会被嵌入到DISTINCT_SCAN的输入中作为其子阶段协同工作。最终缓存的仍是PROJECTION_COVERED → DISTINCT_SCAN的覆盖结构说明在当前数据分布下该管道仍可被覆盖执行。7.3 两种管道的组合验证场景五对$top与$bottom各执行一轮 inactive/active 验证planCacheKey分别稳定为07D4BDAB与4DDFB1B7后者与场景四管道二相同因为两者查询形状一致、只是排序方向不同——从输出看它们共享了缓存条目生命周期印证了形状哈希的判定粒度。完整输出见 distinct_plan_cache.md。八、从源码看缓存条目的生命周期综合以上五个场景可以在源码层面勾勒出计划缓存条目的完整生命周期写入多计划竞争产生获胜计划后PlanCacheEntryBase::create()创建条目plan_cache.h此时isActive为false特例若查询只有一个候选计划、无需计划竞争则通过createPinned()创建固定pinned条目其isActive恒为true且不参与重规划见 plan_cache.h 与isPinned()判断命中与激活后续同形状查询通过get()命中条目若条目为活动状态则直接复用kPresentActive非活动条目被复用时转为活动降级当活动计划开始表现不佳时deactivate()将条目克隆并置为isActive: false使其重新参与后续计划竞争plan_cache.h停用开关可通过参数internalQueryCacheDisableInactiveEntries关闭非活动条目机制deactivate()中loadRelaxed()检查。Golden 测试文档中反复出现的isActive: false → true转变正是第 2、3 步状态迁移的直接观测证据。九、如何复现与扩展验证若要在本地 MongoDB 构建环境中复现这些输出可按以下步骤操作运行驱动测试生成实际输出并与期望输出比对Golden Test 的标准工作流# 使用 resmoke 运行该 golden 测试示例实际命令以构建配置为准 python3 buildscripts/resmoke.py --suitesquery_golden --shellConnString... \ jstests/query_golden/distinct_plan_cache_md.js手动交互验证核心行为对任意集合创建多个复合索引插入含重复x值的数据清空计划缓存后连续执行两次相同的distinct再用$planCacheStats观察缓存条目db.coll.getPlanCache().clear(); db.runCommand({distinct: coll, key: x, query: {x: {$gt: 3}, y: 5}}); db.aggregate([{$planCacheStats: {}}]).toArray(); // 观察 isActive: false db.runCommand({distinct: coll, key: x, query: {x: {$gt: 3}, y: 5}}); db.aggregate([{$planCacheStats: {}}]).toArray(); // 观察 isActive: true查看计划缓存相关的更多资料可参考 README.plan_stability.md 中关于计划稳定性的说明。十、总结本文基于 MongoDB 仓库的 Golden Test 期望输出 distinct_plan_cache.md 及其驱动测试与工具源码完整还原了DISTINCT_SCAN与计划缓存的协作机制可以归纳为以下要点DISTINCT_SCAN计划可以进入计划缓存无论是distinct命令还是可重写为 distinct 的聚合管道$sort $group、$top、$bottom其获胜计划都会按查询形状写入缓存inactive → active 是首次入缓存 → 被复用的观测信号Golden 输出中isActive字段的翻转直接对应 plan_cache.h 中的状态机实现计划缓存按形状寻址不按字面值寻址y: {$lt: 200}与y: {$lt: 250}共享同一planCacheKey只是indexBounds在复用时按新谓词重算DISTINCT_SCAN并非总是最优当集合无重复值时普通IXSCAN可能以更低代价胜出并被缓存这一反直觉行为被 Golden Test 明确固化防止优化器回归Golden Test 是查询优化行为的防回归护栏通过 distinct_plan_cache_md.js golden_test_utils.js 的组合将规划器对 distinct 类查询的缓存决策以 Markdown 形式固化为可评审、可比对、可追溯的契约。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表