MongoDB 聚合 DISTINCT_SCAN 多规划(Multiplanning)策略解析:基于 query_golden 期望输出的深入分析

发布时间:2026/9/13 19:54:55
MongoDB 聚合 DISTINCT_SCAN 多规划(Multiplanning)策略解析:基于 query_golden 期望输出的深入分析
MongoDB 聚合 DISTINCT_SCAN 多规划Multiplanning策略解析基于 query_golden 期望输出的深入分析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文基于 MongoDB 仓库中的查询 golden 测试期望输出文件完整剖析当聚合管道可被改写为distinct去重场景时MongoDB 经典执行引擎classic engine的多规划器如何枚举、比较并选中基于索引的DISTINCT_SCAN候选计划。读完后你将掌握DISTINCT_SCAN与$groupByDistinctScan改写的工作机制、扫描方向与索引选择的确定规则、hint如何强制特定候选、以及rejectedPlans中体现的规划器决策依据并理解 golden 测试如何用期望输出生效锁定这些行为。1. 这份文档从哪里来query_golden 测试框架distinct_aggregation_multiplanning.md 并不是手写文档而是query_golden golden 测试的期望输出golden data它记录了一个 resmoke 测试在指定执行引擎配置下对每一条聚合管道实际产生的 Pipeline、Results、集合索引清单和 Summarized explain 的完整快照。其对应的测试入口是 distinct_aggregation_multiplanning_md.js文件头部声明/** * Tests that the aggregation will go through the process of multiplanning when the pipeline can be * rewritten for the distinct case. * * tags: [ * featureFlagShardFilteringDistinctScan, * requires_fcv_82 * ] */两个标签说明了适用前提测试需要featureFlagShardFilteringDistinctScan特性标志与 FCV 8.2requires_fcv_82。文件名中sbeDisabled前缀体现在期望输出目录名 expected_output/sbeDisabled/表示该变体下 SBE 引擎被禁用因此每份 explain 都标注Execution Engine: classic即所有计划都由经典执行引擎IXSCAN/DISTINCT_SCAN/COLLSCAN等 stage表达。每个测试用例的实际输出由 golden_test_utils.js 中的outputAggregationPlanAndResults该文件第 113 行起生成它先执行coll.aggregate(pipeline, options)收集结果再执行coll.explain().aggregate(pipeline, options)获取 explain然后依次输出Pipeline、可选的Options如 hint、Results、Total indexes on the collection来自coll.getIndexes()以及Summarized explain经formatExplainRoot扁平化后仅保留稳定字段并附带queryShapeHash与stages。这份输出会被与期望输出文件逐行比对——这正是 golden 测试锁定规划器行为的机制一旦 DISTINCT_SCAN 候选的枚举、打分或 tie-break 规则发生变化golden 文件就会失配并暴露出来。1.1 测试的初始数据集与索引期望输出中反复出现的Total indexes on the collection对应测试脚本中建立的 9 个索引初始插入 3 条文档{ _id : 1, a : 4, b : 2, c : 3, d : 4 }, { _id : 2, a : 4, b : 3, c : 6, d : 5 }, { _id : 3, a : 5, b : 4, c : 7, d : 5 }[ _id_, a_1, a_1_b_1, a_-1_b_1, a_1_b_-1, a_1_b_1_c_1, a_1_b_1_d_1, b_1, b_1_a_1, b_1_c_1, d_1_c_-1 ]刻意构造的索引组合覆盖了多种形态复合索引的正/反方向变体a_1_b_1、a_-1_b_1、a_1_b_-1、前缀相同但追加第三列的覆盖型索引a_1_b_1_c_1、a_1_b_1_d_1、字段顺序不同的索引b_1_a_1、d_1_c_-1。这正是 multiplanning 场景同一个$group管道可以对应多个 DISTINCT_SCAN 候选规划器必须从中选出一个获胜者。2. 核心机制DISTINCT_SCAN 与 $groupByDistinctScan 改写当管道形如$sort或$top/$bottom/$first/$last的排序语义 按排序前缀字段的$group时MongoDB 可以将排序 分组取首/尾元素整体改写为一次沿索引顺序的 distinct 扫描索引本身保证了同一_id值内的文档有序扫描器只需记住每组第一个或最后一个文档$group阶段即可直接物化结果无需显式SORTstage甚至无需回表covered。在期望输出的 explain 结构中这一改写体现为两级 stage{ queryShapeHash : 384E008C9BC532E80DBF3FDE111DF54B498D0792E80FC880981635B390F61479, stages : [ { $cursor : { rejectedPlans : [ /* ... */ ], winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1, b : 1 } }, { direction : forward, indexBounds : { a : [ [MinKey, MaxKey] ], b : [ [MinKey, MaxKey] ] }, indexName : a_1_b_1, isFetching : false, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a, accum : $b } } } ] }关键字段解读$cursor/winningPlan游标子计划。winningPlan是自上而下执行的 stage 树rejectedPlans是 multiplanning 中被淘汰的候选每个元素是一条完整候选计划。DISTINCT_SCAN执行引擎中的 distinct 索引扫描 stage。其底层实现位于 distinct_scan.h第 98 行class DistinctScan final : public RequiresIndexStage与 distinct_scan.cpp。头文件中的DistinctParams结构第 40 行起持有indexEntry、indexName、keyPattern、multikeyPaths、isMultiKey、scanDirection1 或 -1对应 explain 里的direction: forward/backward与IndexBounds bounds对应indexBounds字段——也就是说explain 中 DISTINCT_SCAN 的每个字段都直接来自规划时构造的这组参数。$groupByDistinctScan.newRoot被 DISTINCT_SCAN 改写后的$group表达式。注意$first/$last/$top.output都被替换为普通字段引用如accum: $b因为 distinct 扫描已保证每组只产出一条代表文档聚合函数在语义上被吸收进了扫描方向。queryShapeHash查询形状的哈希是 plan cache 的键之一golden 文件中它被完整记录以验证查询归一化常量去除行为稳定。isFetching为false时表示 covered不取回完整文档如配合PROJECTION_COVERED为true时 DISTINCT_SCAN 之后需要 FETCH 文档。3. 场景一仅 DISTINCT_SCAN 候选参与竞争这是期望输出第 1 节## 1. Only DISTINCT_SCAN candidates considered。核心结论当$group的_id与排序前缀完全一致且可用索引能整体满足排序时非 DISTINCT_SCAN 候选不会进入比较规划器只在多个 DISTINCT_SCAN 候选之间选择。3.1 前向扫描sort {a:1, b:1} $first管道与结果[ { $sort : { a : 1, b : 1 } }, { $group : { _id : $a, accum : { $first : $b } } } ]{ _id : 4, accum : 2 } { _id : 5, accum : 4 }获胜计划是a_1_b_1上的正向DISTINCT_SCANdirection: forwardindexBounds为[MinKey, MaxKey]isFetching: false并由PROJECTION_COVEREDtransformBy: {_id:0,a:1,b:1}完成投影。rejectedPlans中有两条同样合法的 DISTINCT_SCAN 候选a_1_b_1_c_1与a_1_b_1_d_1——它们都能提供{a,b}的排序但键更多。从候选集构成看规划器对每一个前缀匹配排序模式的索引都生成了一个 DISTINCT_SCAN 候选multiplanning 打分后淘汰了键更多的两个。3.2 反向扫描sort {a:1, b:-1} $first[ { $sort : { a : 1, b : -1 } }, { $group : { _id : $a, accum : { $first : $b } } } ]结果{ _id : 4, accum : 3 }/{ _id : 5, accum : 4 }注意与 3.1 不同同组内 b 取的是最大值。explain 显示获胜者是a_-1_b_1索引上的反向direction: backwardDISTINCT_SCAN其indexBounds为a: [MinKey, MaxKey]、b: [MaxKey, MinKey]被淘汰的候选是a_1_b_-1的正向扫描。这验证了测试脚本中的注释意图distinct_aggregation_multiplanning_md.js 第 64 行// Ensure the planner correctly reverses the DISTINCT_SCAN direction for $first and $top.即$first的语义是排序后的第一个当没有与排序模式完全同向的索引时规划器可以选择一个方向相反的索引并反转扫描方向backward而不是退化为SORT。3.3 $sort $last 组合{a:-1, b:-1}$last结果与 3.1 相同每组最后一个即全局排序下的最小者获胜计划仍是a_1_b_1正向扫描——$last配反向排序等价于正向扫描取组内首条规划器把$last的取尾语义归一化为 forward DISTINCT_SCAN。同理{a:1, b:-1}$last与{a:-1, b:1}$last两组用例也都分别落在a_-1_b_1的 forward/backward DISTINCT_SCAN 上$groupByDistinctScan.newRoot均为{ _id : $a, accum : $b }。3.4 $top 需要覆盖输出字段索引选择的覆盖优先[ { $group : { _id : $a, accum : { $top : { sortBy : { a : 1, b : 1 }, output : $c } } } } ]结果{ _id : 4, accum : 3 }/{ _id : 5, accum : 7 }输出的是 c。由于output: $c不在排序前缀内只有覆盖 c 的索引才能避免回表。explain 显示获胜计划是a_1_b_1_c_1的 DISTINCT_SCANisFetching: falsePROJECTION_COVERED含crejectedPlans中则是a_1_b_1isFetching: true需要取文档与a_1_b_1_d_1同样isFetching: true两个非覆盖候选。对比$bottom变体无论sortBy是{a:-1, b:-1}还是{a:1, b:-1}只要存在覆盖索引获胜者依然是a_1_b_1_c_1的正向DISTINCT_SCAN——$bottom的取尾语义通过 newRoot 表达accum: $c方向选择交给规划器统一处理。3.5 按 _id 分组单键唯一索引[ { $group : { _id : $_id, accum : { $first : $b } } } ]获胜计划是_id_索引上的正向 DISTINCT_SCAN且 explain 中额外出现isUnique : true其他用例中该字段均为falseisFetching: true表示扫描后需要取回文档以计算$first: $b。rejectedPlans为空——按_id分组时只有_id_索引能给出与分组键一致的顺序。3.6 hint 强制特定 DISTINCT_SCAN测试脚本明确验证了 hint 的两种作用与自动选择一致时保持该计划不一致时覆盖自动选择。// Force particular DISTINCT_SCAN using hint, even if auto-selected by multiplanning. outputAggregationPlanAndResults( coll, [{$sort: {a: 1, b: 1}}, {$group: {_id: $a, accum: {$first: $b}}}], { hint: a_1_b_1 }, ); // Force particular DISTINCT_SCAN using hint, even if different from multiplanning. outputAggregationPlanAndResults( coll, [{$sort: {a: 1, b: 1}}, {$group: {_id: $a, accum: {$first: $b}}}], { hint: a_1_b_1_c_1 }, );期望输出中hint: a_1_b_1用例的 explain 不再包含rejectedPlans枚举为空数组winningPlan 即指定索引的 DISTINCT_SCANhint: a_1_b_1_c_1用例则强制选择了三键覆盖索引indexName: a_1_b_1_c_1isFetching: false且queryShapeHash与无 hint 用例相同384E...说明 hint 不改变查询形状、只约束候选集。对$top输出$c指定hint: a_1_b_1时获胜计划变为a_1_b_1的 DISTINCT_SCAN 且isFetching: true——hint 甚至可以让非覆盖但指定的索引胜出回表代价由规划器照单执行。4. 场景二DISTINCT_SCAN 与非 DISTINCT_SCAN 候选同台竞争第 2 节## 2. Both DISTINCT_SCAN and non-DISTINCT_SCAN candidates considered向集合补插含数组字段的文档{a: 5, b: 4, c: 7, d: [1, 2, 3]}使a_1_b_1_d_1成为 multikey 索引explain 中可见isMultiKey : true、multiKeyPaths : { d : [ d ] }。4.1 DISTINCT_SCAN 胜出对{a:-1, b:-1}排序 $last/$firstrejectedPlans中第一次出现了非 DISTINCT_SCAN 候选a_1_b_1_d_1上的 IXSCANstage : IXSCANmultikey。尽管它也能满足排序multiplanning 打分后仍选择a_1_b_1$last用例为 forward、$first用例为 backward的 DISTINCT_SCAN。从源码结构看DistinctScan与IXSCAN候选在同一候选池中由统一的规划器打分比较multiplanningDISTINCT_SCAN 对排序 去重取代表值这类负载通常以更低的每文档处理成本取胜。4.2 hint 强制非 DISTINCT_SCAN 路径指定hint: a_1_b_1_d_1后explain 结构发生质变winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1, b : 1 } }, { direction : backward, indexName : a_1_b_1_d_1, isMultiKey : true, stage : IXSCAN } ], ... { $group : { $willBeMerged : false, _id : $a, accum : { $last : $b } } }注意$group阶段此时带$willBeMerged : false因为游标不再提供 distinct 保证$group无法被合并进扫描$last聚合函数保持原样。对比 4.1 中$groupByDistinctScan.newRoot形态——这一字段差异是判断改写是否生效的最直观信号。4.3 hint $natural 退化为全表扫描 显式排序hint: {$natural: 1}用例的获胜计划winningPlan : [ { memLimit : 104857600, sortPattern : { a : -1, b : -1 }, stage : SORT, type : simple }, { stage : PROJECTION_SIMPLE, transformBy : { _id : 0, a : 1, b : 1 } }, { direction : forward, stage : COLLSCAN } ]即SORTsimple100MB 内存上限→ PROJECTION_SIMPLE → COLLSCAN$group依旧$willBeMerged: false。这给出了 DISTINCT_SCAN 改写路径的完整反面基线当无法或不允许用索引承载排序时聚合退化为全表扫描 内存排序 普通分组。5. 场景三覆盖投影优先否则选最小索引第 3 节标题即结论DISTINCT_SCAN candidates choose index that covers projection, or smallest index if impossible。三个子用例a无投影选最小索引[ { $group : { _id : $a } } ]结果{ _id : 4 }/{ _id : 5 }。获胜者是单键a_1的 DISTINCT_SCANisFetching: falsePROJECTION_COVERED仅投影a。此时$group: {_id: $a}等价于对 a 的 distinct 查询最小前缀索引即可覆盖。b存在覆盖投影的索引选之[ { $group : { _id : $a, accumB : { $first : $b }, accumC : { $first : $c } } } ]需要 a、b、c 三个字段。获胜计划是a_1_b_1_c_1的 DISTINCT_SCANisFetching: falsenewRoot: { _id: $a, accumB: $b, accumC: $c }。a_1/a_1_b_1等短索引虽然更小但无法覆盖 b、c需要回表因此在候选比较中落败。c无索引可覆盖投影退回最小索引[ { $group : { _id : $a, accumB : { $first : $b }, accumC : { $first : $c }, accumD : { $first : $d } } } ]需要 a、b、c、d 四个字段没有任何一个索引同时覆盖a_1_b_1_c_1缺 da_1_b_1_d_1缺 c。获胜者退回最小的a_1索引且isFetching: true——扫描只提供 a 的顺序b/c/d 全部依赖回表取文档。6. 场景四根级 $or 的边界可合并性第 4 节标题给出了规则Rooted $or can only use a DISTINCT_SCAN when all predicates have mergeable bounds for a single index scan。DISTINCT_SCAN 依赖单一索引上的一次有序遍历因此 $or 的各分支谓词必须能归并为同一索引字段上的多个区间。6.1 可合并单字段双分支 $or[ { $match : { $or : [ { a : { $lte : 5 } }, { a : { $gt : 8 } } ] } }, { $group : { _id : $a } } ]获胜计划是a_1的 DISTINCT_SCAN其indexBounds为合并后的双区间indexBounds : { a : [ [-inf, 5.0], (8.0, inf] ] }rejectedPlans里既有同样合并了边界的 DISTINCT_SCAN 候选a_1_b_1、a_-1_b_1、a_1_b_-1、a_1_b_1_c_1、a_1_b_1_d_1也有以ORstage 展开的非 distinct 候选PROJECTION_DEFAULT → OR → IXSCAN(a_1_b_1) IXSCAN(a_1)等 5 条。规划器在一次合并区间扫描与OR 双路扫描之间选中了前者。6.2 不可合并同字段叠加 $and[ { $match : { $or : [ { a : { $lte : 5 } }, { a : { $gt : 6, $lt : 7 } }, { a : { $gt : 8 } } ] } }, { $group : { _id : $a } } ]中间分支{a: {$gt: 6, $lt: 7}}与首尾分支产生三个互不重叠的区间[-inf,5.0]、(6.0,7.0)、(8.0,inf]无法作为单一 distinct 扫描的合并边界表达。获胜计划变为SUBPLAN → PROJECTION_DEFAULT → OR → IXSCAN(a_1, 双区间) IXSCAN(a_1, (6.0,7.0))$group为普通分组$willBeMerged: false。6.3 不可合并$or 跨不同字段[ { $match : { $or : [ { a : { $gt : 0 } }, { b : { $lt : 10 } } ] } }, { $group : { _id : $a } } ]以及更复杂的{a: {$gt:0}, b: {$lt:10}} OR {a: {$lt:10}}变体两者的获胜计划均为SUBPLAN → OR → IXSCAN(b_1_a_1) IXSCAN(a_1)第二个变体为IXSCAN(a_1) IXSCAN(a_1_b_1)均无 DISTINCT_SCAN 候选——不同字段的分支无法共享一个索引的有序遍历。6.4 同分平局与选择率比较测试还构造了两个专门集合验证 tie-breakDISTINCT_SCAN 与 IXSCAN 平局时偏向 DISTINCT_SCAN集合仅含 5 条{a: i, b: -i}文档与{a:-1,b:1}、{a:1,b:1}两个索引。对{$match: {a: {$gt: 0}}}$group/_id:$a/$top获胜计划是a_1_b_1的 forward DISTINCT_SCANPROJECTION_COVEREDrejectedPlans中是被淘汰的a_-1_b_1反向 IXSCAN 候选。b 上谓词选择率更高时偏向 FETCH filter IXSCAN20 条{a: i, b: -i, c: i%5}文档索引{a:1,b:1}与{b:1,c:1}。查询{$match: {a: {$gt: 0}, b: {$gt: -18}}}中b 的谓词只过滤掉 2 条比 a 的谓词选择性强。获胜计划变为PROJECTION_SIMPLE → FETCH → IXSCAN(b_1_c_1, b: [-18.0, inf])一类的非 distinct 计划DISTINCT_SCANa_1_b_1进入rejectedPlans。这说明 DISTINCT_SCAN 并非无条件偏好——当另一个索引的选择性明显更好时普通 IXSCAN FETCH 可以胜出。7. 场景五冲突的排序规格使 DISTINCT_SCAN 候选消失第 5 节的两个用例中$sort与$top/$bottom的sortBy语义冲突[ { $sort : { a : 1, b : 1 } }, { $group : { _id : $a, accum : { $top : { sortBy : { b : 1, a : 1 }, output : $c } } } } ]$sort要求全局序为(a,b)而$top的sortBy是(b,a)——测试脚本注释distinct_aggregation_multiplanning_md.js 第 204 行解释The $sort is incompatible with the sortBy, so a distinct scan cant provide both sorts. 因为一次索引遍历只能给出一个全序无法同时充当两种排序DISTINCT_SCAN 候选为空。获胜计划退化为覆盖扫描PROJECTION_COVERED → IXSCAN(a_1_b_1_c_1)无 FETCH$group保留完整$top表达式且$willBeMerged: false由分组 stage 自身维护 top 语义。第二个用例$sort {a:1,b:1}$bottom {sortBy: {a:-1, b:-1}}的注释第 209 行指出// This query could (and after SERVER-94369, possibly will) be answered by a forward distinct scan // on a_1_b_1.即该形态可能被 forward DISTINCT_SCAN 回答引用了工作项 SERVER-94369表明这是一个演进中的能力但在当前期望输出中它仍以a_1_b_1_c_1覆盖 IXSCAN 获胜。golden 文件把这种当前行为快照固化下来任何行为变化都会通过 golden 比对显式暴露。8. 场景六multikey 索引排除 DISTINCT_SCAN 候选第 6 节先补插含数组字段的文档如{a: [1, 2, 3], b: 4, c: 7, d: 5}使 a 变为 multikey 字段$sort {a:1,b:1}$group/_id:$a/$first:$ba 是 multikey 字段按 a 去重取首条语义与 multikey 索引的重复键条目不兼容。rejectedPlans含a_1_b_1_c_1、a_1_b_1_d_1的 FETCH IXSCAN 计划获胜计划为PROJECTION_SIMPLE → FETCH → IXSCAN(a_1_b_1)$group为普通分组。$group: {_id: $a}按 multikey 字段 distinct获胜计划直接是COLLSCANPROJECTION_SIMPLE → COLLSCAN因为没有任何索引能在 multikey 语义下给出 a 的 distinct 有序序列。从 distinct_scan.h 的DistinctParams结构看isMultiKey与multikeyPaths是候选构造的一等输入构造时即通过collection-isIndexMultikey(...)填充期望输出第 4 节中 multikey DISTINCT_SCAN 候选a_1_b_1_d_1虽被生成但仍被打分淘汰而本节中按 multikey 字段本身分组则根本不产生候选——两者共同界定了 multikey 对 distinct 改写的影响边界。9. 场景七按非 multikey 字段分组、对 multikey 字段取 $first/$last第 7 节把角色对调分组键 b 是普通字段$first/$last的取值字段 a 是 multikey 字段。[ { $group : { _id : $b, accum : { $first : $a } } } ]结果{ _id : 2, accum : 4 } { _id : 3, accum : 4 } { _id : 4, accum : [ 1, 2, 3 ] }b4 组内 a 恰为数组[1,2,3]。获胜计划$first: $a→b_1索引forwardDISTINCT_SCANisFetching: true需回表取 multikey 的 anewRoot: { _id: $b, accum: $a }$last: $a→b_1索引backwardDISTINCT_SCAN。这说明 multikey 限制只作用于扫描顺序必须按 multikey 字段建立的情形分组键为非 multikey 字段时distinct 扫描按分组键推进、取值字段通过 FETCH 获得改写依然成立。9.1 平局规则DISTINCT_SCAN 之间键数最少者胜末尾子节 Multiplanning tie between DISTINCT_SCANs favors fewest index keys 在只含 5 条普通文档的集合上建立 5 个前缀递增索引a_1到a_1_b_1_c_1_d_1_e_1执行{$match: {a: {$gt: 0}}}$group: {_id: $a}。rejectedPlans依次列出了a_1_b_1_c_1_d_1、a_1_b_1_c_1、a_1_b_1、a_1_b_1_c_1_d_1_e_1四个候选边界均为a: [(0.0, inf]]获胜者是键数最少的a_1PROJECTION_COVERED DISTINCT_SCAN。这与 3.1、5(a) 的观察一致在投影可覆盖的前提下索引键数越少扫描的键比较与区间维护开销越低multiplanning 打分越优。10. 规划器决策规则汇总综合期望输出 7 个场景可以归纳出 DISTINCT_SCAN 候选的生成与选择规则均可在上文 golden 文件对应小节中找到解释证据决策点规则证据小节候选生成每个能整体承载排序模式含反向索引 backward方向的索引生成一个 DISTINCT_SCAN 候选1.xrejectedPlans枚举改写生效标志$group变为$groupByDistinctScan.newRoot$first/$last/$top被折叠为字段引用1.x 全部 explain投影覆盖优先选择能 covered 投影的索引isFetching: false不可覆盖时选最小索引并回表isFetching: true3.x、3.4平局DISTINCT_SCAN vs IXSCAN偏向 DISTINCT_SCAN6.4平局DISTINCT_SCAN vs DISTINCT_SCAN键数最少者胜9.1选择性其他索引谓词选择率明显更高时FETCH IXSCAN 可胜 DISTINCT_SCAN6.4$or 根级谓词仅当所有分支可归并为单索引合并边界时可用否则退化 OR/SUBPLAN4.x排序冲突$sort与sortBy全序冲突时不生成 DISTINCT_SCAN 候选5.xmultikey按 multikey 字段分组/排序时排除对应候选按非 multikey 分组仍可用6.x、7.xhint强制指定候选可覆盖 multiplanning 结果此时rejectedPlans为空1.6、2.2复现与查阅路径期望输出本文主体distinct_aggregation_multiplanning.mdsbeDisabled、sbeFull、sbeRestricted等目录对应不同引擎开关下的同名快照测试脚本与用例编排distinct_aggregation_multiplanning_md.jsgolden 输出工具outputAggregationPlanAndResults等golden_test_utils.js执行引擎中 distinct 扫描 stage 的参数结构与实现distinct_scan.h、distinct_scan.cpp。在本地 MongoDB 实例上可以按 1.1 节创建同样的索引并插入 3 条文档然后执行db.coll.explain().aggregate([...])或db.coll.aggregate([...], {hint: ...})对照本文各小节的 explain 结构重点观察winningPlan中的stage: DISTINCT_SCAN、direction、isFetching与外层的$groupByDistinctScan即可逐一验证上述规则。这套 golden 测试的意义正在于此它把规划器看不见的决策——候选枚举、打分、淘汰——变成了可审查、可回归的文本快照。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考