
MongoDB 分片去重查询优化DISTINCT_SCAN 与 Chunk Skipping 的 Golden 测试与 Explain 全解析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文以 MongoDB 仓库jstests/query_golden_sharding下的distinct_chunk_skipping黄金测试为切入点完整还原分片sharded环境下$match $group去重查询在不同索引、不同选择率条件下如何选择DISTINCT_SCAN执行计划以及chunk skipping分块跳过、分片过滤shard filtering与嵌入式FETCH何时生效。读完本文你将能读懂$groupByDistinctScan、isShardFiltering、isFetching、chunkSkips等 explain 字段的真实含义掌握选择性查询跳过 chunk、非选择性查询启用 chunk skipping、无前缀索引时退化为 IXSCAN FETCH SHARDING_FILTER这一整套分片去重优化决策规律。一、背景分片环境下去重查询的三重挑战在分片集群中一个[{$match: {...}}, {$group: {_id: $a}}]聚合会被 mongos 拆分为shardsPart下发到各分片与mergerPart在 mongos 端合并。分片端要高效完成去重需要同时解决三个问题去重扫描本身$group对$a去重时最优的物理执行方式是在以a为前缀的索引上做DISTINCT_SCAN跳过索引中相邻的重复键值孤儿文档orphans过滤chunk 迁移后旧分片上可能残留不属于本分片 chunk 范围的文档这些孤儿文档必须被SHARDING_FILTER剔除否则结果错误范围剪枝若能根据 chunk 的键范围直接跳过整段索引区间chunk skipping可以大幅减少扫描量——这正是本测试的核心主题。测试脚本头部注释明确写道Tests the results and explain output for multiplanning DISTINCT_SCANs in cases where the extra work of embedded chunk skipping is evaluated during plan enumeration.即该测试专门验证在计划枚举阶段是否评估并选择嵌入 chunk skipping 额外开销的 DISTINCT_SCAN 计划。二、测试全景测试脚本与 Golden 输出如何联动2.1 测试脚本测试主体位于 distinct_chunk_skipping.js其执行流程如下使用ShardingTest拉起2 个分片每个分片是副本集并设置disableResumableRangeDeleter: true——禁用范围删除器从而保证孤儿文档在测试期间如预期般残留让计划选择可预测设置TestData.skipCheckOrphans true并在注释中说明Deliberately inserts orphans刻意制造孤儿文档在test.distinct_chunk_skipping集合上创建索引{a: 1, c: 1}与{a: 1, b: 1, c: 1}插入 15 条文档字段a的值刻意设计为shard0_1、shard0_2、shard1_1……——a的值前缀直接编码了文档逻辑上属于哪个分片方便验证 chunk 划分与孤儿残留分片前先执行第 1 个场景未分片基线随后shardCollection以{a: 1, b: 1, c: 1}为片键split后再moveChunk把shard1_*的数据块搬到shard1制造部分分片上有对方 chunk 的孤儿文档的局面。2.2 Golden 输出的生成机制文档中的 Pipeline / Results / Total indexes on the collection / Summarized explain 四段式结构由 golden_test_utils.js 中的outputAggregationPlanAndResults()生成它依次输出 pipeline、排序后的结果数组、集合全部索引名以及经过formatExplainRoot()扁平化后的 explain 摘要测试脚本再用section()为每个场景写入标题。因此.md文件就是可回归比对的金标准输出——如果 MongoDB 的优化器行为发生变化导致 explain 或结果不一致该测试就会失败。2.3 测试标签与运行前提测试脚本的tags声明了运行前提也直接说明本文主题依赖的功能开关标签含义featureFlagShardFilteringDistinctScan分片过滤型 DISTINCT_SCAN将 SHARD_FILTERING/FETCH 内嵌进 DISTINCT_SCAN 的能力featureFlagGetExecutorDeferredEngineChoice延迟选择执行引擎的功能开关待 SERVER-128587 移除requires_fcv_82需要 featureCompatibilityVersion 82 及以上的服务器版本resource_intensive资源密集型测试详见 TODO SERVER-101494期望未来降低 burn_in 任务数三、场景 1未分片环境使用带嵌入式 FETCH 的 DISTINCT_SCAN基线这是分片前的基线场景用于对照后面分片后的行为差异。Pipeline[ { $match : { a : { $gte : shard0 }, c : { $eq : 1 } } }, { $group : { _id : $a } } ]Results{ _id : shard0_1 } { _id : shard0_2 } { _id : shard1_2 }Total indexes on the collection[ _id_, a_1_c_1, a_1_b_1_c_1 ]Summarized explain{ distinct_chunk_skipping-rs0 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [1.0, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : false, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [1.0, 1.0] ] }, indexName : a_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : false, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ], queryShapeHash : 2209486C2E1164C3906C2002ABE4D156E6CA7B9B8F8679EA27FE4B8E819792B5 }关键解读两个候选计划都基于DISTINCT_SCANa_1_b_1_c_1被拒与a_1_c_1胜出。因为c上有等值约束c 1且只需要对a去重更窄的{a, c}索引边界a: [shard0, {})、c: [1.0, 1.0]即可覆盖全部谓词无需回表isFetching: false、isShardFiltering: false单分片节点上 explain 不含mergeType、mergerPart、shardsPart只有$cursor$groupByDistinctScan$groupByDistinctScan.newRoot: {_id: $a}表示去重分组由 DISTINCT_SCAN 直接完成queryShapeHash用于标识查询形状可辅助计划缓存plan cache与查询统计的关联分析。四、场景 2选择性查询——DISTINCT_SCAN shard filtering 嵌入式 FETCH但不做 chunk skipping分片并迁移 chunk 后同一查询的执行计划发生了关键变化。Pipeline[ { $match : { a : { $gte : shard0 }, c : { $eq : 1 } } }, { $group : { _id : $a } } ]Results{ _id : shard0_1 } { _id : shard0_2 } { _id : shard1_2 }Total indexes on the collection[ _id_, a_1_c_1, a_1_b_1_c_1 ]Summarized explain{ distinct_chunk_skipping-rs0 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [1.0, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [1.0, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ], distinct_chunk_skipping-rs1 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [1.0, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [1.0, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ], mergeType : router, mergerPart : [ { $mergeCursors : { allowPartialResults : false, compareWholeSortKey : false, nss : test.distinct_chunk_skipping, requestQueryStatsFromRemotes : false, tailableMode : normal } }, { $group : { $doingMerge : true, _id : $$ROOT._id } } ], queryShapeHash : 2209486C2E1164C3906C2002ABE4D156E6CA7B9B8F8679EA27FE4B8E819792B5, shardsPart : [ { $match : { $and : [ { a : { $gte : shard0 } }, { c : { $eq : 1 } } ] } }, { $group : { _id : $a } } ] }关键解读与场景 1 相比分片后的 explain 出现三个显著变化mergeType: routermergerPartshardsPartmongos 在mergerPart用$mergeCursors拉取各分片游标并执行$group {$doingMerge: true, _id: $$ROOT._id}做最终去重合并shardsPart则是下发到每个分片的$match $groupisShardFiltering: true由于存在孤儿文档风险SHARD_FILTERING逻辑被内嵌进 DISTINCT_SCAN 执行isFetching: true表示 DISTINCT_SCAN 不再全覆盖需要回表取文档以执行分片过滤片键{a,b,c}与索引{a,c}不完全一致过滤必须结合完整文档/片键判定但没有chunk skipping本节标题即为 Selective query uses DISTINCT_SCAN shard filtering embedded FETCH, but no chunk skipping。原因是c 1是等值点查询命中区间很窄chunk 边界与查询范围重叠程度有限计划枚举阶段评估后认为嵌入 chunk skipping 的额外开销不划算——注意测试脚本注释the extra work of embedded chunk skipping is evaluated during plan enumeration即是否启用 skipping 是计划枚举阶段通过成本评估做出的决策。五、场景 3非选择性查询——DISTINCT_SCAN shard filtering 嵌入式 FETCH chunk skipping把c的约束从等值$eq: 1放宽为范围$lte: 1同一套测试环境下的决策立刻不同。Pipeline[ { $match : { a : { $gte : shard0 }, c : { $lte : 1 } } }, { $group : { _id : $a } } ]Results{ _id : shard0_1 } { _id : shard0_2 } { _id : shard0_3 } { _id : shard1_1 } { _id : shard1_2 }Total indexes on the collection[ _id_, a_1_c_1, a_1_b_1_c_1 ]Summarized explain{ distinct_chunk_skipping-rs0 : [ { $cursor : { rejectedPlans : [ [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [-inf, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [-inf, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ], distinct_chunk_skipping-rs1 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [-inf, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [-inf, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ], mergeType : router, mergerPart : [ { $mergeCursors : { allowPartialResults : false, compareWholeSortKey : false, nss : test.distinct_chunk_skipping, requestQueryStatsFromRemotes : false, tailableMode : normal } }, { $group : { $doingMerge : true, _id : $$ROOT._id } } ], queryShapeHash : B9BC1E78DD6A3A08A3F84BAFFEFAEDBFFFE732B016DE718C9C056C098CADA9CA, shardsPart : [ { $match : { $and : [ { a : { $gte : shard0 } }, { c : { $lte : 1 } } ] } }, { $group : { _id : $a } } ] }关键解读两个分片上的胜出计划不同rs0shard0副本集胜出的是三字段索引a_1_b_1_c_1isFetching: false索引覆盖查询rs1胜出的是a_1_c_1isFetching: true需要回表。这说明分片各自的索引统计与代价评估独立进行被拒计划中的a_1_c_1rs0边界为c: [-inf, 1.0]是范围查询$lte展开后的完整下界——范围越宽候选计划的区分度越明显chunk skipping 在本场景生效标题 Non-selective query uses DISTINCT_SCAN shard filtering embedded FETCH chunk skipping因为c 1命中范围大、扫描跨度长通过片键范围直接跳过本分片并不拥有的连续索引区间收益显著。反观场景 2 的等值查询命中范围极窄skipping 的收益无法覆盖其开销故被计划枚举阶段否决。选择率selectivity是决定 chunk skipping 启停的关键因素两个场景的queryShapeHash不同2209...对应$eqB9BC...对应$lte说明查询形状区分了运算符差异计划缓存不会互相串用。六、场景 4无 DISTINCT_SCAN——退化为 shard filtering FETCH filter再增加一个{c: 1}单字段索引后集合上有 4 个索引优化器评估出的最优计划彻底改变。Pipeline[ { $match : { a : { $gte : shard0 }, c : { $eq : 1 } } }, { $group : { _id : $a } } ]Results{ _id : shard0_1 } { _id : shard0_2 } { _id : shard1_2 }Total indexes on the collection[ _id_, a_1_c_1, a_1_b_1_c_1, c_1 ]Summarized explain{ distinct_chunk_skipping-rs0 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [1.0, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ], [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [1.0, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { stage : PROJECTION_SIMPLE, transformBy : { _id : 0, a : 1 } }, { stage : SHARDING_FILTER }, { filter : { a : { $gte : shard0 } }, nss : test.distinct_chunk_skipping, stage : FETCH }, { direction : forward, indexBounds : { c : [ [1.0, 1.0] ] }, indexName : c_1, isMultiKey : false, isPartial : false, isSparse : false, isUnique : false, keyPattern : { c : 1 }, multiKeyPaths : { c : [ ] }, nss : test.distinct_chunk_skipping, stage : IXSCAN } ] } }, { $group : { _id : $a } } ], distinct_chunk_skipping-rs1 : [ { $cursor : { rejectedPlans : [ [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], b : [ [MinKey, MaxKey] ], c : [ [1.0, 1.0] ] }, indexName : a_1_b_1_c_1, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, b : 1, c : 1 }, multiKeyPaths : { a : [ ], b : [ ], c : [ ] }, stage : DISTINCT_SCAN } ], [ { direction : forward, indexBounds : { a : [ [\shard0\, {}) ], c : [ [1.0, 1.0] ] }, indexName : a_1_c_1, isFetching : true, isMultiKey : false, isPartial : false, isShardFiltering : true, isSparse : false, isUnique : false, keyPattern : { a : 1, c : 1 }, multiKeyPaths : { a : [ ], c : [ ] }, stage : DISTINCT_SCAN } ] ], winningPlan : [ { stage : PROJECTION_SIMPLE, transformBy : { _id : 0, a : 1 } }, { stage : SHARDING_FILTER }, { filter : { a : { $gte : shard0 } }, nss : test.distinct_chunk_skipping, stage : FETCH }, { direction : forward, indexBounds : { c : [ [1.0, 1.0] ] }, indexName : c_1, isMultiKey : false, isPartial : false, isSparse : false, isUnique : false, keyPattern : { c : 1 }, multiKeyPaths : { c : [ ] }, nss : test.distinct_chunk_skipping, stage : IXSCAN } ] } }, { $group : { _id : $a } } ], mergeType : router, mergerPart : [ { $mergeCursors : { allowPartialResults : false, compareWholeSortKey : false, nss : test.distinct_chunk_skipping, requestQueryStatsFromRemotes : false, tailableMode : normal } }, { $group : { $doingMerge : true, _id : $$ROOT._id } } ], queryShapeHash : 2209486C2E1164C3906C2002ABE4D156E6CA7B9B8F8679EA27FE4B8E819792B5, shardsPart : [ { $match : { $and : [ { a : { $gte : shard0 } }, { c : { $eq : 1 } } ] } }, { $group : { _id : $a } } ] }关键解读本场景标题为 No DISTINCT_SCAN on a虽然a_1_b_1_c_1与a_1_c_1两个 DISTINCT_SCAN 候选都被枚举并放入rejectedPlans但优化器最终选择了IXSCAN(c_1) → FETCH(带 a shard0 的 filter) → SHARDING_FILTER → PROJECTION_SIMPLE的经典三段式计划这说明当存在一个能让过滤更高效的单字段索引c_1直接命中等值谓词c 1时即使 DISTINCT_SCAN 可行优化器也会基于代价否决它——DISTINCT_SCAN不是有就必用而是参与多计划竞争本场景中分片端聚合阶段也退化为普通$group {_id: $a}不再有$groupByDistinctScan去重逻辑由 group 自行完成PROJECTION_SIMPLE而非PROJECTION_COVERED配合FETCH.filter中的a: {$gte: shard0}印证了回表后还需在文档层再过滤a的语义。七、源码印证explain 字段的来源与内嵌阶段的统计合并golden 输出里的isShardFiltering、isFetching、chunkSkips、orphanChunkSkips等字段并非手写而是由查询执行引擎在 explain 序列化时输出的。在 plan_explainer_impl.cpp 中可以找到对应实现DISTINCT_SCAN 节点第 290–293 行当spec-isShardFilteringDistinctScanEnabled为真即featureFlagShardFilteringDistinctScan生效时才向 explain 追加isShardFiltering与isFetching两个布尔字段。这解释了为什么本文 explain 中这两个字段总是成对出现且仅在特性开启后可见DISTINCT_SCAN 的 executionStats第 442–454 行注释明确指出 Because we push FETCH and SHARD_FILTERING stages into the DISTINCT_SCAN stage when applicable, we dont see FETCHs docsExamined or SHARD_FILTERINGs chunkSkips in the explain output因此执行引擎会把内嵌 FETCH 与 SHARD_FILTERING 的统计合并计入 DISTINCT_SCAN追加输出docsExamined、chunkSkips、orphanChunkSkips。由此可以确认本文第 4、5 节的观察isShardFiltering: true DISTINCT_SCAN 内嵌了分片过滤必须剔除孤儿文档isFetching: true 内嵌了回表 FETCH索引无法覆盖过滤所需的片键信息chunk skipping 本质上是 SHARD_FILTERING 在 DISTINCT_SCAN 语境下的范围级跳过优化当扫描范围宽非选择性时按本分片所拥有的 chunk 区间直接跳过不归属本分片的连续索引段chunkSkips/orphanChunkSkips即统计这类跳过的次数范围窄选择性时该优化收益有限计划枚举阶段选择不启用。八、分片去重 explain 字段速查表字段出现位置含义stage: DISTINCT_SCAN分片端 $cursor 内使用索引有序扫描跳过重复键值完成去重$groupByDistinctScan.newRoot分片端聚合段去重分组直接由 DISTINCT_SCAN 驱动isShardFilteringDISTINCT_SCAN 节点是否内嵌 SHARD_FILTERING存在孤儿过滤需求isFetchingDISTINCT_SCAN 节点是否内嵌 FETCH 回表chunkSkips/orphanChunkSkipsDISTINCT_SCAN 的 executionStats按 chunk 范围/连续孤儿段跳过的次数isMultiKey/isPartial/isSparse/isUniqueDISTINCT_SCAN 节点索引属性影响 DISTINCT_SCAN 可行性indexBounds扫描节点各字段的谓词边界如[shard0, {})、[-inf, 1.0]、[1.0, 1.0]mergeType: routerexplain 顶层合并发生在 mongosrouter端mergerPartexplain 顶层mongos 端$mergeCursors$group {$doingMerge: true}去重合并shardsPartexplain 顶层下发给各分片的$match $group片段queryShapeHashexplain 顶层查询形状哈希用于计划缓存与查询统计关联九、如何复现与运行该测试是 MongoDB 仓库内的 JS 集成测试采用 golden 比对机制。运行前提服务器需启用featureFlagShardFilteringDistinctScan与featureFlagGetExecutorDeferredEngineChoice见测试脚本tags并要求requires_fcv_82使用 resmoke 执行仓库根目录下python3 buildscripts/resmoke.py run jstests/query_golden_sharding/distinct_chunk_skipping.js运行后生成/比对的 golden 文件位于 featureFlagSbeFull/distinct_chunk_skipping.mdSBE 全量开启时的期望输出即本文分析的文档仓库中还同时维护了 sbeDisabled/distinct_chunk_skipping.mdSBE 关闭时的期望输出用于覆盖不同执行引擎下的行为一致性。若执行结果与 golden 文件不一致测试即失败——这正是用可读的 Markdown锁定优化器行为的回归防护手段。十、总结通过distinct_chunk_skipping黄金测试的四个场景可以提炼出分片集群中$group去重查询的计划选择规律索引前缀决定 DISTINCT_SCAN 的可行性需要以去重字段a为索引前缀c的等值/范围谓词决定具体使用的复合索引分片后必然内嵌分片过滤只要集合已分片且存在孤儿风险DISTINCT_SCAN 就携带isShardFiltering必要时再内嵌FETCHisFetchingchunk skipping 是选择率驱动的成本决策等值、窄范围查询选择性高不启用 chunk skipping宽范围查询选择性低启用按 chunk 边界跳过不归属本分片的索引区间DISTINCT_SCAN 并非总是最优当单字段索引如c_1能把过滤成本压得更低时优化器会选择IXSCAN FETCH SHARDING_FILTER的常规计划两个 DISTINCT_SCAN 候选则进入rejectedPlans。这一整套行为由 distinct_chunk_skipping.js 通过刻意制造孤儿文档 禁用范围删除器的受控实验加以验证并由 plan_explainer_impl.cpp 中的 explain 序列化逻辑落地为可观测的字段。理解这些字段与决策规则是排查分片集群上去重聚合慢问题的第一手依据。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考