
MongoDB 分片查询黄金测试哈希分片键上的复合索引与 DISTINCT_SCAN 执行计划解读【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本篇以 MongoDB 仓库中的分片查询黄金输出文件sharded_distinct_scan_hashed_compound_ix.md为核心讲清楚一个典型场景当集合使用{a: hashed, m: 1}这样的哈希范围混合分片键且集合上存在一个字段顺序相反的复合索引a_1_m_hashed时$group与distinct()为什么都会走上DISTINCT_SCAN阶段并逐段解读其 explain 输出结构。读完本文你可以掌握分片集群下 distinct 类查询的索引覆盖机制、mergerPart/shardsPart解释计划形态以及 MongoDB 黄金数据测试框架Golden Data Test如何固化并校验这类查询计划。一、测试场景哈希分片键与反向复合索引该黄金输出文件由测试 sharded_distinct_scan_hashed_compound_ix.js 生成属于query_golden_sharding测试套件。测试的搭建逻辑如下见测试源码 jstests/query_golden_sharding/sharded_distinct_scan_hashed_compound_ix.js#L16-L34通过new ShardingTest({shards: 1})搭建一个单分片1 个 replica set的集群库test启用分片并指定主分片对test.coll执行分片分片键为{a: hashed, m: 1}——a是哈希分片键m是范围分片键插入 5 个文档m与a的值分别为(0, 0.0)、(0.1, 0.1)、(0.2, 0.2)、(0.2, 0.9)、(0.2, 1.9)即a字段 5 个值互不相同m字段存在重复在集合上显式创建复合索引{a: 1, m: hashed}索引名为a_1_m_hashed。这里的命名hashed_compound_ix正源于此分片键是{a: hashed, m: 1}而待测索引是{a: 1, m: hashed}两者的哈希/范围角色互相对调构成一个容易被查询计划选错的边界场景。测试随后验证两个查询// $group by field that is hashed on shard key but not on index outputAggregationPlanAndResults(coll, [{$group: {_id: $a}}]); // distinct() on field that is hashed in shard key but in index outputDistinctPlanAndResults(coll, a);二、第 1 节$group by field that is hashed on shard key but not on index这是黄金文件 sharded_distinct_scan_hashed_compound_ix.md#L1-L69 记录的第一段内容对应管道[ { $group : { _id : $a } } ]2.1 结果{ _id : 0 } { _id : 0.1 } { _id : 0.2 } { _id : 0.9 } { _id : 1.9 }$group按_id: $a分组a的 5 个值互不相同因此返回 5 个分组。注意分组键$a恰好是分片键中的哈希字段a: hashed但索引a_1_m_hashed里a的范围分量在索引中排在第一位——这个在分片键上是哈希、在索引上却是范围前缀的错位正是本测试要锁定的行为。2.2 集合上的索引[ _id_, a_1_m_hashed ]集合上共有两个索引默认的_id_索引和显式创建的a_1_m_hashed。2.3 汇总后的 explain$groupByDistinctScan改写 DISTINCT_SCAN{ queryShapeHash : D9C9FECEDCF44552BDAE97948C54C49831832F11DC812AF429C85FE39BABE26F, sharded_distinct_scan_hashed_compound_ix-rs0 : [ { $cursor : { rejectedPlans : [ ], winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [MinKey, MaxKey] ], m : [ [MinKey, MaxKey] ] }, indexName : a_1_m_hashed, isFetching : false, isMultiKey : false, isPartial : false, isShardFiltering : false, isSparse : false, isUnique : false, keyPattern : { a : 1, m : hashed }, stage : DISTINCT_SCAN } ] } }, { $groupByDistinctScan : { newRoot : { _id : $a } } } ] }几个关键事实可以从这段输出读出$groupByDistinctScan聚合改写MongoDB 的聚合查询优化器识别出$group的_id只是一个简单字段引用$a于是把它改写成对a字段做 distinct 扫描newRoot: { _id : $a }记录了改写前的原始分组表达式。改写后根本不需要走常规的分组哈希只需拿到a的去重值即可。DISTINCT_SCAN阶段直接扫描索引a_1_m_hashed提取a的去重值。indexBounds中a和m都是[MinKey, MaxKey]表示全索引扫描去重isFetching: false表示这是纯索引覆盖操作不回表读取文档数据。PROJECTION_COVERED位于计划顶层其transformBy: { _id : 0, a : 1 }表明投影仅涉及_id与a两个字段全部由索引条目供给。rejectedPlans为空数组优化器没有产生任何被拒绝的候选计划说明在该场景下DISTINCT_SCAN是唯一且确定的胜选计划——这正是黄金测试要冻结的行为。输出按节点名sharded_distinct_scan_hashed_compound_ix-rs0组织体现单分片单副本集下的解释结构queryShapeHash是查询形状的哈希指纹用于跨版本对比同一逻辑查询的计划。三、第 2 节distinct()on field that is hashed in shard key but in index黄金文件 sharded_distinct_scan_hashed_compound_ix.md#L71-L122 的第二段记录distinct: a、过滤条件为{}的 distinct 查询Distinct on a, with filter: { } ### Distinct results [ 0, 0.1, 0.2, 0.9, 1.9 ]distinct 结果与$group分组结果一致5 个去重值。3.1 分片 distinct 的 explain 结构mergerPartshardsPart{ mergerPart : [ { stage : SINGLE_SHARD } ], queryShapeHash : 0D1E1B86B99E41A2D9F60B1CA65B9CB0BFDC2A31FAFB84E78CF5F8EECB797CF6, shardsPart : { sharded_distinct_scan_hashed_compound_ix-rs0 : { rejectedPlans : [ ], winningPlan : [ { stage : PROJECTION_COVERED, transformBy : { _id : 0, a : 1 } }, { direction : forward, indexBounds : { a : [ [MinKey, MaxKey] ], m : [ [MinKey, MaxKey] ] }, indexName : a_1_m_hashed, isFetching : false, keyPattern : { a : 1, m : hashed }, stage : DISTINCT_SCAN } ] } } }mergerPartmongos 合并层的计划。由于本测试只有 1 个分片合并层退化为SINGLE_SHARD阶段——直接把该分片的结果透传给客户端无需真正的多分片 distinct 归并。shardsPart分片层的计划结构上与第一节的winningPlan完全相同PROJECTION_COVERED覆盖DISTINCT_SCAN(a_1_m_hashed)rejectedPlans为空。这证明distinct命令与$groupByDistinctScan改写共用同一条索引去重扫描路径。两段queryShapeHash不同是因为一个是 aggregate 查询形状、一个是 distinct 命令形状但二者在分片内部的执行形态一致。3.2 为什么DISTINCT_SCAN能成立从这两个计划可以看出选型的实质条件目标字段a在索引a_1_m_hashed中作为前缀字段出现keyPattern.a 1distinct 所需的全部字段a与_id都能从索引条目中直接读取因此执行器可以在不回表isFetching: false的情况下完成去重。即便a是分片键中的哈希字段数据在逻辑上被哈希打散索引本身仍然按a的范围顺序组织条目去重扫描不受哈希分片影响——这是本黄金文件验证的核心结论。四、黄金输出是如何生成与校验的该.md文件是黄金数据测试框架Golden Data Test的期望输出测试机制在 docs/golden_data_test_framework.md 中有完整说明测试运行时把实际输出写到actual目录与仓库中 check-in 的期望输出做比较任何差异都会使测试失败并生成output_path/actual/test_path与output_path/expected/test_path供 diff 检查。具体到本套件运行入口配置见 buildscripts/resmokeconfig/suites/query_golden_sharding.yml其selector匹配jstests/query_golden_sharding/**/*.js并在eval钩子中调用beginGoldenTest(jstests/query_golden_sharding/expected_output, .md)把本目录所有测试的输出统一落到expected_output/下的.md文件输出的 Markdown 格式化由 jstests/libs/query/golden_test_utils.js 完成outputAggregationPlanAndResults()负责打印 Pipeline、Results、Total indexes on the collection 与 Summarized explain 各小节与黄金文件的章节结构一一对应outputDistinctPlanAndResults()负责 distinct 查询小节explain 输出的扁平化由 jstests/libs/query/analyze_plan.js 中的formatExplainRoot、getStableExecutionStats等辅助函数完成其中getShardsFromExplain展示了分片 explain 中queryPlanner.winningPlan.shards的抽取逻辑这正是第三节shardsPart结构的来源。当查询计划或结果发生变化导致黄金文件 diff 失败时仓库提供 buildscripts/golden_test.py 工具进行批量 diff 与 accept# 首次使用先做一次性工作站配置 buildscripts/golden_test.py setup export GOLDEN_TEST_CONFIG_PATH~/.golden_test_config.yml # 对比最近一次测试运行的输出差异 buildscripts/golden_test.py diff # 将 actual 输出批量接受为新的期望输出 buildscripts/golden_test.py accept # 一次更新同一测试在多个 passthrough/构建变体下的期望文件 buildscripts/golden_test.py --verbose clean-run-accept jstests/query_golden_sharding/sharded_distinct_scan_hashed_compound_ix.js按框架的最佳实践期望输出文件不应手工修改而应重新运行测试后用accept更新且对测试 fixture 的改动应单独提交 PR以便评审 diff。五、小结与延伸该黄金文件锁定的行为可以概括为一句话对于分片键{a: hashed, m: 1}的集合若存在索引a_1_m_hashed$group {_id: $a}经$groupByDistinctScan改写与distinct: a都在分片内部选择DISTINCT_SCAN PROJECTION_COVERED的纯索引计划且无候选计划被拒绝单分片下 distinct 的 explain 呈现mergerPart: [SINGLE_SHARD]shardsPart的两段结构多分片时mergerPart会退化为真正的 distinct 归并计划可从同目录其他测试如 distinct_chunk_skipping.js、distinct_scan_multi_chunk.js中对照阅读适用前提该测试带有requires_fcv_82标签见 测试文件头部要求服务端 feature compatibility version 不低于 8.2 才可运行若你想在本地复现只需按query_golden_sharding.yml中的 selector 运行 resmoke 套件再结合buildscripts/golden_test.py diff查看任何计划漂移。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考