资讯动态

MongoDB 分片集合 $group 下推(Pushdown)行为解析:基于 query_golden_sharding 黄金测试的全面验证

发布时间:2026/9/14 10:08:45 来源:尧图企业网站定制
MongoDB 分片集合 $group 下推Pushdown行为解析基于 query_golden_sharding 黄金测试的全面验证【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文以 MongoDB 仓库中 group_targeting.js 黄金测试及其预期输出文档为骨架系统讲解在分片集合上执行$group聚合时MongoDB 查询优化器在什么条件下可以把$group完整下推fully push down到各分片、什么条件下只能部分下推partial pushdown、什么条件下完全不能下推并通过逐条 explain 证据帮助读者建立分片键感知的 $group 优化的完整心智模型。读完本文你将能够根据_id与分片键的关系、聚合 collation、上游 stage 对分片键的改写情况快速判断一个$group能否被下推并读懂分片聚合 explain 中$willBeMerged、$doingMerge、DISTINCT_SCAN、$groupByDistinctScan等关键标记。一、背景为什么要研究分片集合上的 $group 下推在 MongoDB 分片集群中聚合管道默认由 mongosrouter将管道分发到各分片执行再由 router 合并结果。对于$group这类需要先按组归类、再做累加的算子能否在分片上预先完成分组直接决定跨分片传输的数据量完整下推每个分片只输出本分片分组后的结果router 只需做轻量合并甚至无需再次$group网络与内存开销最小部分下推分片先做一轮分组router 再用$doingMerge: true的$group合并两端分工明确完全不下推分片把原始文档或投影后的文档全部上送 router由 router 统一分组代价最高。判断下推可行性的核心依据只有一个$group的_id分组键与集合分片键的关系。仓库中的黄金测试 group_targeting.js 用 2 个分片、两组数据集单分片键{shardKey: 1}与复合分片键{sk0: 1, sk1: 1, sk2: 1}穷举了数十种场景并把每一种场景的聚合结果与 explain 计划固化到 group_targeting.md是该特性的行为契约。该测试文件顶部注明了运行前提group_targeting.js依赖featureFlagGetExecutorDeferredEngineChoice、featureFlagShardFilteringDistinctScan两个特性开关并要求 FCV 不低于 8.2requires_fcv_82。而本文对应的预期输出目录名featureFlagSbeAccumulatorExpressions关联的特性开关定义在 query_feature_flags.idlfeatureFlagSbeAccumulatorExpressions: description: Feature flag to enable SBE lowering of accumulators used as expressions, i.e. $sum, $min, $max, $avg, $stdDevPop, $stdDevSamp and $mergeObjects outside of $group. cpp_varname: gFeatureFlagSbeAccumulatorExpressions default: false fcv_gated: false incremental_rollout_phase: in_development该开关默认关闭、处于 in_development 阶段作用是让累加器accumulator表达式在 SBE 引擎中降级执行这正是本组黄金测试所要验证的 SBE 聚合行为。二、黄金测试的数据布局读懂 explain 的前置条件所有场景共用同一份精心构造的数据group_targeting.js。理解这份数据是读懂后续所有 explain 的关键集合以{shardKey: 1}分片在shardKey: shard1处 split 并把shard1_*所在 chunk 移到shard1otherShard测试注释特别强调一条易错数据{_id: 6.5, shardKey: shARD1_3}因为shARD1_3 shard1_1大写字母的字典序更小它实际上落在 shard0而分片键大小写混用的数据shard0_1与shARD1_3等专门用于验证下推后分组键是否被正确保留、是否出现重复_id集合上只有两个索引_id_和shardKey_1这就是每个场景中Total indexes on the collection恒为[ _id_, shardKey_1 ]的原因复合分片键集合则以{sk0: 1, sk1: 1, sk2: 1}分片在{sk0: s0/1, sk1: 1, sk2: h}处 split 并移动 chunkgroup_targeting.js索引为sk0_1_sk1_1_sk2_1。每个场景的 Pipeline / Results / Options / Total indexes / Summarized explain 均由 golden_test_utils.js 中的outputAggregationPlanAndResults()生成它执行coll.aggregate(pipeline, options)取结果执行coll.explain().aggregate(...)取执行计划再把 explain 扁平化flatten成下文看到的紧凑结构。这也是为什么文档里每个场景的 explain 字段如$cursor、winningPlan、mergerPart、shardsPart完全一致、格式统一。三、完整下推当_id 分片键单分片键3.1 最简单的形态_id: $shardKey管道只有一步[ { $group : { _id : $shardKey } } ]由于分组键与分片键完全一致每个分片内的分组结果天然互不重叠router 无需二次分组。从 explain 看这个场景获得了三重优化叠加见文档第一节第一个场景的完整 JSON索引覆盖扫描winningPlan 使用shardKey_1索引做DISTINCT_SCANindexBounds为shardKey: [[MinKey, MaxKey]]isFetching: false无需回表取文档、isShardFiltering: true索引本身过滤掉不属于本分片的孤儿数据router 侧零分组mergerPart只有$mergeCursors没有$groupshardsPart 标记$willBeMerged: false明确告诉读者此$group的结果不会被 router 再次合并——这是完整下推的标志性字段。注意_id的结果里有sHaRd0_2、shARD1_3这样的大小写变体它们与shard0_2、shard1_3都是不同的分组键简单排序规则下因此每个都作为独立组输出结果条数与全表文档去重后的分片键取值一一对应。3.2 可走 DISTINCT_SCAN 的管道前缀 $group当$group不是管道终点而是管道前缀且满足 distinct scan 的约束后续$match只缩小范围、$sort与分组键序一致时下推依然成立。文档中的第二个场景[ { $group : { _id : $shardKey, otherField : { $top : { output : $otherField, sortBy : { shardKey : 1 } } } } }, { $match : { _id : { $lte : shard1_1 } } }, { $sort : { _id : 1 } } ]其中$top累加器按shardKey升序取每个组内otherField的第一个值等价于每组内按索引序取首文档——这正好与 DISTINCT_SCAN 的输出顺序契合。explain 显示shardsPart 变为三段$matchshardKey shard1_1→$group$willBeMerged: false→$sortwinningPlan 仍是DISTINCT_SCAN但indexBounds变为[, shard1_1]isFetching: true因为$top需要取回otherField字段投影无法完全覆盖router 侧$mergeCursors携带sort: {_id: 1}说明由 router 完成最终排序结果中{ _id : shard0_1, otherField : a }与{ _id : shard0_3, otherField : c }等条目顺序正确$top语义被忠实保留。3.3 简单 rename 不破坏下推下推判定跟踪的是分组键是否可回溯到分片键因此管道前加一步$project改名不影响判定[ { $project : { renamedShardKey : $shardKey } }, { $group : { _id : $renamedShardKey } } ]explain 中$projectPROJECTION_DEFAULTtransformBy: {_id: true, renamedShardKey: $shardKey}被下推到分片$group仍以$willBeMerged: false完整下推。可见优化器具备简单的字段沿袭lineage追踪能力renamedShardKey是shardKey的纯改名分组键因此仍是分片键。四、累加器正确性验证$avg 的下推文档第二节验证了带累加器的$group在下推后计算仍然正确[ { $group : { _id : $shardKey, avg : { $avg : $_id } } } ]结果{ _id : shard0_1, avg : 1.25 }、{ _id : shard0_2, avg : 2 }等与手算一致shard0_1 组内_id为 1 和 1.5平均 1.25shard0_2 组内_id为 2shard0_2和 2.5sHaRd0_2也落在该组不——2.5 的分片键是sHaRd0_2它与shard0_2是不同的键平均 2。这一场景的 explain 结构给出了一个与 3.1 的重要对照当$group带普通累加器如$avg时不再使用 DISTINCT_SCANwinningPlan 退化为GROUP → SHARDING_FILTER → COLLSCAN。原因是$avg必须扫描组内所有文档计算均值无法从索引的 distinct 序直接得出。这也解释了黄金测试为何同时覆盖无累加器与有累加器两类DISTINCT_SCAN 优化只适用于_id恰好可由索引枚举且无需全组扫描的场景。该场景的$group依然以$willBeMerged: false下推因为_id shardKey保证了各分片的组集合两两不相交router 无须合并。若要理解$willBeMerged这一标记的完整语义可参考 document_source_group.cpp 中对该标志的设置与消费逻辑它决定后续 merge 阶段是否生成$doingMerge: true的合并$group。五、分组键是分片键的超集superset仍可完整下推当_id由分片键 其他字段组成时虽然组集合不再互斥两个分片都可能出现shard0_1组但由于_id中包含了完整的分片键router 仍可仅凭_id值判断哪些组需要跨分片合并因此依旧允许完整下推由 router 用$doingMerge: true的$group做最后合并。5.1 数组形式的超集键[ { $group : { _id : [ $shardKey, $_id ] } } ]结果中出现了[ shard0_1, 1 ]与[ shard0_1, 1.5 ]两个不同组同一分片键、不同_id充分体现超集含义。explain 的shardsPart中$group带$willBeMerged: false而mergerPart在$mergeCursors之后追加了一个$group: { $doingMerge: true, _id: $$ROOT._id }——$willBeMerged: false$doingMerge: true是一对配套标记前者表示分片侧分组会被 router 消费后者表示 router 侧执行合并分组。5.2 管道前缀 超集键在 5.1 的$group后追加$match与$sort的场景文档第三节第二个用例shardsPart 变为$group → $match → $sort三段winningPlan 内出现MATCH → GROUP → SHARDING_FILTER → COLLSCANrouter 侧$mergeCursors携带sort: {_id: 1}。注意此处$match被下推到分片执行分片先按_id shard1_1过滤再做组内$top减少了上送 router 的数据量。5.3 对象形式的超集键 rename[ { $project : { renamedShardKey : $shardKey } }, { $group : { _id : { secretlyShardKey : $renamedShardKey, actualId : $_id } } } ]_id是一个嵌套文档其中secretlyShardKey字段回溯到分片键经一次改名actualId为_id。该场景同样完整下推分片执行$project → $group$willBeMerged: falserouter 用$doingMerge按_id合并。注意结果对象中字段顺序被稳定排序actualId在前、secretlyShardKey在后与 BSON 排序规则一致。六、只能部分下推分组键由分片键派生当_id不是分片键本身或其超集/子集的纯投影而是对分片键做了表达式变换时下推退化为部分下推——分片照常执行$grouprouter 必须用$doingMerge: true合并。文档第三节的三个用例均属此类场景管道中的_id结果特点explain 特征$min([$shardKey, 1])常量折叠全表只剩{ _id : 1 }一组$doingMerge: true分片侧_id中常量被改写为{$const: 1}$min([$shardKey, 1])$top累加器 后续$match/$sort同上空结果集$doingMerge: true的$group还要对otherField做$top合并sortBy: {shardKey: 1}$min([$shardKey, $_id])依赖其他字段输出 11 个不同数值每个文档一个$min结果$doingMerge: true以第三个用例为例$min([$shardKey, $_id])对每个文档求分片键字符串与数值_id的较小者结果因类型比较规则几乎总是_id本身故得到 11 个互不相同的组。此场景不能完整下推因为无法仅凭分片键判断分片 A 的组1.5与分片 B 的组1.5是否同组——_id中不含分片键信息。explain 中mergerPart的$group使用$doingMerge: true且_id: $$ROOT._id即按已分组结果的_id直接归并。这类派生键场景的 SBE 下推行为正是 query_feature_flags.idl 中featureFlagSbeAccumulatorExpressions所控制的累加器表达式降级优化的一部分测试目录名即由此而来。七、多 $group 串联逐段判定下推策略黄金测试用四个用例覆盖了一个管道里出现多个$group的组合情况核心结论是每个$group独立判定且依赖其上游_id表达式第一组在分片键上、第二组在其派生字段上_id: {key: $shardKey, other: $otherField}后接_id: $_id.other第一个$group以$willBeMerged: false完整下推第二个$group只能在分片执行后在 router 用$doingMerge: true合并两个组都在分片键上第一组后接_id: $_id即恒等变换按注释could actually push both down但当前优化器只下推了第一个$group第二个$group_id: $_id在分片执行后仍需 router 合并——这属于可优化但暂未完全优化的已知边界完整下推 部分下推组合_id: $shardKey, avg后接_id: $avg, num: {$count: {}}第一个$group完整下推第二个$group在分片统计$sum: {$const: 1}router 用$doingMerge的$sum: $$ROOT.num汇总——注意合并累加器从$count被重写为$sum常量这正是合并阶段累加器改写的典型证据上述规则在复合分片键集合上同样成立见文档第四节末段两个用例。八、完全不下推的六类场景负向用例黄金测试后半部分系统列出了必须禁止完整下推的情形这些是防止产生错误结果尤其重复_id的底线约束分组键不是分片键_id: $otherField。explain 中分片侧$group无$willBeMerged: falserouter 必须$doingMerge。即使先用$project把otherField改名为shardKey再分组也不会被下推——改名前的源字段不是分片键分片键未保留$project: {otherField: 1}把分片键投影掉之后$group: {_id: $shardKey}得到{ _id : null }所有文档的分片键均为缺失值归为一组分片键被 $addFields 覆盖$addFields: {shardKey: $otherField}后按_id: $shardKey分组此时当前管道中的 shardKey已非原分片键只能部分下推分组键为整个文档_id: $$ROOT。虽然文档含分片键字段但整体分组无法保证组不相交只能部分下推而_id: $$ROOT.shardKey显式取$$ROOT上的分片键字段则可完整下推并走 DISTINCT_SCAN——下推判定要求分组键表达式直接就是分片键字段引用聚合 collation 比分片键更粗当聚合指定collation: {locale: en_US, strength: 2}大小写不敏感时shARD1_3与shard1_3会按 collation 被视为同一组若下推则不同分片的同组文档会被错误拆分。文档在该用例前专门加了警告Note: If we have duplicate _ids in the output, that signals a bug here.若输出出现重复_id即说明此处有 bug并额外用$addFields: {_id: {$toLower: $_id}}统一大小写以便观察最终结果只有 6 个组shard0_1、shard0_2、shard0_3、shard1_1、shard1_2、shard1_3证明没有错误下推分片键字段是后来才添加的$project: {shardKey: 0}先删除分片键$addFields: {shardKey: $otherField}再添加同名新字段随后按_id: $shardKey分组——由于该字段值来自otherField不能被当作分片键只能部分下推。explain 里可见分片侧依次执行PROJECTION_SIMPLE → PROJECTION_DEFAULT($addFields) → GROUP的完整链路。以上 6 类负向场景的共同原则可归纳为一句管道当前状态下的分组键必须可精确回溯lineage到分片键字段且分组语义不受 collation 影响二者缺一不可。九、复合分片键集合下的下推矩阵第四节针对复合分片键{sk0, sk1, sk2}复测了全部关键场景其结论与单分片键完全对齐但补充了三个单键场景没有的维度完整下推 累加器_id {sk0, sk1, sk2}同时计算$sum: $sk1与$addToSet: $sk2$willBeMerged: false结果中sumSk1正确聚合如s0/1, sk12组求和得 4完整下推 DISTINCT_SCAN_id {sk0, sk1, sk2}且累加器为$bottomoutput: $$ROOTsortBy: {sk0: 1, sk1: 1, sk2: 1}——$bottom只需要每组按分片键排序的最后一个文档与复合索引序完全一致因而该场景保留了 DISTINCT_SCAN 能力与 3.1 一样索引被充分利用_id是分片键的真子集subset_id: {sk0: $sk0, sk2: $sk2}缺少sk1无法仅凭分组键判断跨分片同组关系只能部分下推$doingMerge: true非简单 rename$project中对sk1做{$add: [1, $sk1]}变换后再分组即便sk0、sk2是纯改名只要有一个分片键分量被表达式改写整体就只能部分下推包含非分片键字段且非简单 rename先$addFields: {complex: {$rand: {}}}引入随机字段第一个$group的_id为{sk0, complex, sk1, sk2}超集但含非 rename 字段随后第二个$group再投影回{sk0, sk1, sk2}。此处两个$group都以$willBeMerged: false出现在shardsPart中、无需 router 合并——因为第二个$group的分组键恰好又是完整分片键。该矩阵完整对应了文档第四节中从Pushdown works for simple $group where _id shard key到Push down $group which includes a field that is not the shardKey的全部 8 个子用例可作为复合分片键设计的直接参考。十、如何运行与复现本组黄金测试本组测试属于 jstest 黄金测试体系生成与校验预期输出的逻辑封装在 golden_test_utils.jsoutputAggregationPlanAndResults负责产出 Pipeline/Options/Results/Indexes/Explain 五段式 markdown运行入口即 group_targeting.js测试文件通过 BUILD.bazel 纳入 Bazel 的mongo_js_library目标expected_output/下的 markdown 即各特性组合的期望产物。同一测试在仓库中并存多份输出sbeDisabled、sbeFull、sbeRestricted、featureFlagSbeFull、featureFlagSbeAccumulatorExpressions目录下均有group_targeting.md用于对比不同引擎/特性开关下的行为差异本文基于其中的featureFlagSbeAccumulatorExpressions版本。复现要点依据 group_targeting.js 的测试标签构建并启动支持 FCV 8.2 及以上的 mongod/mongos通过--setParameter启用featureFlagGetExecutorDeferredEngineChoice与featureFlagShardFilteringDistinctScan执行mongo jstests/query_golden_sharding/group_targeting.js或经由 Bazel 对应测试目标运行结果与预期输出比对任何场景下若结果出现重复_id、或 explain 中的 stage 序列与文档不一致即代表下推逻辑回归。十一、总结一张表看懂 $group 下推决策分组键_id的形态下推级别explain 标志文档依据场景节等于单分片键字段完整 DISTINCT_SCAN$willBeMerged: falsewinningPlan 含DISTINCT_SCAN第三节 3.1/3.2等于分片键 普通累加器完整$willBeMerged: falseGROUP → COLLSCAN第四节分片键字段的简单 rename完整分片执行$project后再$group第三节 3.3、第九节分片键的超集数组/对象完整分片$willBeMerged: falserouter$doingMerge: true第五节由分片键经表达式派生部分router$doingMerge: true分片侧常量改写为$const第六节分片键的真子集部分router$doingMerge: true第九节非分片键 / 分片键被投影或覆盖 /$$ROOT不下推仅分片预聚合分片$group无$willBeMerged第八节 1-4、6任意键但聚合 collation 非简单不下推router 必须合并第八节 5黄金测试的价值在于把上述决策固化成了可自动比对的行为契约任何改动只要让某个场景的 explain 或结果偏离 group_targeting.md 中的记录测试就会失败。对于希望在分片集群上获得最佳聚合性能的开发者本文的实践要点同样适用尽量让$group的_id等于或包含完整分片键、避免对分片键做表达式改写、避免使用与分片键排序不一致的 collation即可最大化利用分片内预聚合与 DISTINCT_SCAN 优化把跨分片数据传输降到最低。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价