
MongoDB 分片 count 命令的 limit/skip 行为基于 query_golden_sharding 黄金数据测试解读 explain executionStats【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文基于 MongoDB 仓库中的黄金数据Golden Data测试jstests/query_golden_sharding/sharded_explain_count_with_limit_skip.js及其预期输出文件 sharded_explain_count_with_limit_skip.md完整剖析在分片集群上执行带limit/skip的count命令时mongos 路由层SHARD_MERGE与单分片SINGLE_SHARD两种拓扑下的 explainexecutionStats语义。读完后你可以掌握如何从 explain 输出的limitAmount/skipAmount/nCounted/isEOF/totalKeysExamined字段判断 limit 与 skip 究竟在哪一层生效、每个分片实际扫描了多少键以及黄金数据测试框架如何固化这些行为防止回归。1. 这是一类什么样的测试文件该.md文件并不是手写文档而是黄金数据测试的预期输出快照expected output。根据 golden_data_test_framework.md 的描述黄金数据测试运行被测代码把确定性输出与仓库中已签入的已知正确输出逐字节比对任何差异都会导致测试失败必须更新代码或预期输出文件之一。它特别适合输出无法用客观性质直接验证的场景——count在分片集群上的 explain 统计字段正属于此类nCounted应该是合并后的总数还是各分片之和skipAmount应该出现在哪一层这些行为没有公开的性质断言只能靠签入的黄金输出来锁定。生成这些输出的工具链如下均已确认存在于仓库sharded_explain_count_with_limit_skip.js测试主体构造 2 分片拓扑、插入数据、逐场景执行count命令和explaingolden_test_utils.js其中outputCountPlanAndResults先把count命令结果与executionStats.nCounted做断言assert.eq(actualCount, executionStages.nCounted)再调用outputCommonPlanAndResults输出 Query/Results/索引列表/汇总后的 explainoutputCommonPlanAndResults还会断言executionStages.limitAmount、skipAmount与测试期望值一致analyze_plan.js提供getStableExecutionStats从 explain 中提取稳定字段剔除不稳定的内存、时间字段后压平为扁平计划再经tojsonMultiLineSortKeys按键排序输出保证跨运行确定性pretty_md.jssection()/subSection()产生.md中## 1./### Query等标题结构。测试文件的注释明确了被测目标sharded explain for count commands correctly tracks limit and skip values on the router when targeting multiple shards并且verifies that nCounted reflects the merged total, not the sum across shards验证nCounted反映的是合并后的总数而不是各分片计数之和。文件头部的标签requires_fcv_82、assumes_read_concern_local表明该行为从 FCV 8.2 起被断言且依赖local读关注级别。2. 测试拓扑与数据分布测试代码sharded_explain_count_with_limit_skip.js的关键设置const st new ShardingTest({mongos: 1, shards: 2, config: 1}); const coll assertDropAndRecreateCollection(db, jsTestName()); // 集合名即 jsTestName()故命令中 count 的参数是 sharded_explain_count_with_limit_skip db.adminCommand({enableSharding: db.getName()}); db.adminCommand({shardCollection: coll.getFullName(), key: {x: 1}}); // 插入 _id: 0..99, x: 0..99 共 100 条文档 for (let i 0; i 100; i) bulk.insert({_id: i, x: i}); db.adminCommand({split: coll.getFullName(), middle: {x: 50}}); // 下界 chunk [minkey, 50) 移到 shard0 db.adminCommand({moveChunk: ..., find: {x: 45}, to: st.shard0.shardName, ...}); // 上界 chunk [50, maxkey) 移到 shard1 db.adminCommand({moveChunk: ..., find: {x: 55}, to: st.shard1.shardName, ...});由此得到确定性的数据分布这也是 8 组用例数值可以手工推算的基础分片chunk 范围包含的 x 值文档数shard0[minkey, 50)0–4950shard1[50, maxkey)50–9950集合上只有两个索引默认_id_与分片键索引x_1——这正是每份预期输出中Total indexes on the collection: [ _id_, x_1 ]一节的来源由outputAvailableIndexes打印全部索引名。每个场景通过runCountAndExplain依次执行三步运行count命令取res.n运行{explain: cmdObj, verbosity: executionStats}调用outputCountPlanAndResults把查询命令、结果数字、索引列表和汇总后的 explain executionStats写入输出流。3. 八组预期输出全解以下按原文件顺序逐节给出完整内容并附数据推算。注意 count 类查询的 explain 有一个贯穿所有场景的特征totalDocsExamined恒为0——COUNT阶段只需要索引键即可计数不需要抓取数据文档因此只有totalKeysExamined有值。3.1 场景 1多分片 简单 limit查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 30 } }, limit : 5 }结果5汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { limitAmount : 5, nCounted : 5, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 5, stage : IXSCAN }, isEOF : 0, nReturned : 5, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 5, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 5 }, { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 5, stage : IXSCAN }, isEOF : 0, nReturned : 5, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 5, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 5 } ], stage : SHARD_MERGE, totalDocsExamined : 0, totalKeysExamined : 10 } } ]解读x 30实际命中 69 条shard0 有 31–49 共 19 条shard1 有 50–99 共 50 条但limit: 5被下推到每个分片各分片IXSCAN只返回 5 条即停止isEOF: 0表示未读尽索引。顶层SHARD_MERGE持有limitAmount: 5totalKeysExamined: 1055。关键点是SHARD_MERGE的nCounted为 5 而非 10——各分片COUNT的nCounted都是 5但路由层统计的是合并流上真正计数的 5 条。nReturned: 0是因为 count 命令不产生文档只产生计数。3.2 场景 2多分片 简单 skip无 limit查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 45, $lt : 55 } }, skip : 5 }结果4汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { nCounted : 4, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 4, stage : IXSCAN }, isEOF : 1, nReturned : 4, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 4, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 4 }, { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 5, stage : IXSCAN }, isEOF : 1, nReturned : 5, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 5, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 5 } ], skipAmount : 5, stage : SHARD_MERGE, totalDocsExamined : 0, totalKeysExamined : 9 } } ]解读查询命中 46–54 共 9 条分片分布为 shard0 4 条46–49、shard1 5 条50–54。这里体现了与场景 1 不同的一个行为skip 没有被下推到分片——两个分片都读尽了全部匹配键isEOF: 1分别 4 和 5 个键各自COUNT的nCounted是跳过前的 4 和 5skipAmount: 5出现在顶层SHARD_MERGE阶段说明 skip 在路由层合并流上生效最终nCounted 9 - 5 4。这正是测试注释所说的nCounted 是合并总数不是分片之和分片之和为 9而路由层为 4。3.3 场景 3多分片 limit 与 skip 同时存在查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 30 } }, limit : 5, skip : 5 }结果5汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { limitAmount : 5, nCounted : 5, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 10, stage : IXSCAN }, isEOF : 0, nReturned : 10, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 10, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 10 }, { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 10, stage : IXSCAN }, isEOF : 0, nReturned : 10, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 10, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 10 } ], skipAmount : 5, stage : SHARD_MERGE, totalDocsExamined : 0, totalKeysExamined : 20 } } ]解读当 limit 存在时下推到分片的量是skip limit 10因此每个分片扫描 10 个键后停止isEOF: 0分片侧COUNT的nCounted为 10路由层合并流上再施加skipAmount: 5与limitAmount: 5最终SHARD_MERGE.nCounted为 5总扫描键数 20。这个每分片多读 skip 量的代价可以从totalKeysExamined: 20直接观察到——如果 limit/skip 只在路由层生效而不下推理论键数可能更多如果完全不感知 limit两个分片会读尽 69 条。3.4 场景 4命中数少于 limitnCounted 低于 limit查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gte : 49, $lte : 51 } }, limit : 5 }结果3汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { limitAmount : 5, nCounted : 3, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 1, stage : IXSCAN }, isEOF : 1, nReturned : 1, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 1, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 1 }, { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 2, stage : IXSCAN }, isEOF : 1, nReturned : 2, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 2, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 2 } ], stage : SHARD_MERGE, totalDocsExamined : 0, totalKeysExamined : 3 } } ]解读命中 49shard01 条与 50、51shard12 条共 3 条。计划中仍然携带limitAmount: 5limit 出现在路由层计划里是确定的但由于每个分片先读尽索引isEOF: 1最终nCounted: 3低于 limit。该场景锁定的行为是limit 不足量时不应报错或截断统计nCounted忠实反映实际命中数。3.5 场景 5命中数少于 skip limit查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gte : 47, $lte : 55 } }, limit : 5, skip : 5 }结果4汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { limitAmount : 5, nCounted : 4, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 3, stage : IXSCAN }, isEOF : 1, nReturned : 3, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 3, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 3 }, { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 6, stage : IXSCAN }, isEOF : 1, nReturned : 6, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 6, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 6 } ], skipAmount : 5, stage : SHARD_MERGE, totalDocsExamined : 0, totalKeysExamined : 9 } } ]解读命中 47–55 共 9 条shard0 3 条、shard1 6 条少于skip limit 10两个分片均读尽索引isEOF: 1。路由层对 9 条合并流先跳 5 再限 5得到nCounted: 4。与场景 3 对照可知下推到分片的最多读skiplimit个键只是上界实际键数取决于各分片真实命中量本例 369 2×10。3.6 场景 6单分片 简单 limitSINGLE_SHARD查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 90 } }, limit : 5 }结果5汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { nCounted : 5, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 5, stage : IXSCAN }, isEOF : 0, nReturned : 5, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 5, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 5 } ], stage : SINGLE_SHARD, totalDocsExamined : 0, totalKeysExamined : 5 } } ]解读x 90的匹配区间 [91, 100) 完全落在 shard1 的 chunk [50, maxkey) 内查询计划器判定只涉及单个分片顶层阶段从SHARD_MERGE变为SINGLE_SHARDshards数组只有一项。此时的一个重要差异顶层SINGLE_SHARD阶段不再出现limitAmount/skipAmount字段——limit/skip 被整体下压进分片内部管道路由层退化为透传统计量nCounted: 5、totalKeysExamined: 5与分片侧完全一致。这解释了为什么该场景在测试代码中的期望是expected: {stage: SINGLE_SHARD}而不带limit/skip断言。3.7 场景 7单分片 简单 skip查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 90 } }, skip : 5 }结果4汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { nCounted : 4, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 1, nReturned : 9, stage : IXSCAN }, isEOF : 1, nReturned : 9, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 4, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 9 } ], stage : SINGLE_SHARD, totalDocsExamined : 0, totalKeysExamined : 9 } } ]解读91–99 共 9 条全部在 shard1 上IXSCAN读尽 9 个键isEOF: 1但分片侧COUNT的nCounted直接是4——skip 在单分片拓扑下被下推到分片内部的 count 管道计数发生在跳过后9-54。与场景 2 对照即可看出 skip 生效位置随拓扑变化的两条路径多分片时在SHARD_MERGEskipAmount出现在路由层分片侧nCounted为跳过前数值单分片时在分片内部顶层无skipAmount分片侧nCounted已是跳过后数值。3.8 场景 8单分片 limit 与 skip 同时存在查询{ count : sharded_explain_count_with_limit_skip, query : { x : { $gt : 80 } }, limit : 5, skip : 5 }结果5汇总后的 explain executionStatsExecution Engine: classic[ { executionStages : { nCounted : 5, nReturned : 0, shards : [ { executionStages : { inputStage : { inputStage : { isEOF : 0, nReturned : 10, stage : IXSCAN }, isEOF : 0, nReturned : 10, stage : SHARDING_FILTER }, isEOF : 1, nCounted : 5, nReturned : 0, stage : COUNT }, nReturned : 0, totalDocsExamined : 0, totalKeysExamined : 10 } ], stage : SINGLE_SHARD, totalDocsExamined : 0, totalKeysExamined : 10 } } ]解读81–99 共 19 条在 shard1 上下推量为skip limit 10IXSCAN读 10 个键后停止isEOF: 0分片侧COUNT完成跳 5 留 5nCounted: 5顶层SINGLE_SHARD统计与分片一致。4. 八组用例的行为规律汇总把上述 8 份预期输出横向对比可以提炼出该黄金数据文件实际锁定的行为矩阵场景拓扑limitskip顶层阶段limitAmount/skipAmount 位置分片侧扫描键顶层 nCounted1SHARD_MERGE5—SHARD_MERGE路由层 limitAmount5×252SHARD_MERGE—5SHARD_MERGE路由层 skipAmount45读尽43SHARD_MERGE55SHARD_MERGE路由层两者都有10×254SHARD_MERGE5—SHARD_MERGE路由层 limitAmount12读尽35SHARD_MERGE55SHARD_MERGE路由层两者都有36读尽46SINGLE_SHARD5—SINGLE_SHARD下推至分片557SINGLE_SHARD—5SINGLE_SHARD下推至分片9读尽48SINGLE_SHARD55SINGLE_SHARD下推至分片105其中三条最值得注意的语义结论均有对应预期输出作为证据多分片时 limit/skip 由路由层持有并施加于合并流对应 explain 顶层的limitAmount/skipAmount字段分片收到的下推量是 limit 场景下的limit与 skiplimit 场景下的skip limit而纯 skip 场景下分片不做截断、路由层统一跳数。顶层nCounted是合并流上的计数不是分片nCounted之和场景 2 中分片之和为 9 而顶层为 4场景 3 中分片各为 10 而顶层为 5。count 只考键不考文档所有场景totalDocsExamined为 0性能分析时应以totalKeysExamined评估扫描代价isEOF: 0标记因 limit 提前停止isEOF: 1标记该分片索引已读尽。5. 测试如何固化这些行为从 sharded_explain_count_with_limit_skip.js 的runCountAndExplain与 golden_test_utils.js 可以看到双重校验数值断言每次运行都执行count命令返回的res.n必须等于 explain 的executionStats.nCountedlimitAmount/skipAmount必须与场景期望值expected: {stage, limit, skip}相等顶层stage必须与期望的SHARD_MERGE/SINGLE_SHARD一致。全文比对黄金数据经getStableExecutionStats稳定化、按键排序后的 explain 与本文第 3 节展示的签入预期文件逐字比对任何字段哪怕只是totalKeysExamined从 10 变成 11都会导致测试失败。该测试注册在 jstests/query_golden_sharding/BUILD.bazel 中属于query_golden_sharding套件。若要查看或更新预期输出可参考 golden_data_test_framework.md 中diff and accept流程使用buildscripts/golden_test.py的setup/diff/accept子命令配置outputRootPattern与diffCmd并设置GOLDEN_TEST_CONFIG_PATH环境变量该文档同时强调预期输出文件是自动生成的不应手工修改而应运行测试后把 actual 输出复制为新的 expected 文件。6. 对使用者的实践意义这份黄金数据文件对调优分片count查询有直接指导价值当count命中多个 chunk 时若带 limit每个被触及分片至多扫描limit或skip limit个键代价近似线性于被触及分片数而非数据总量——从totalKeysExamined可精确核算当查询谓词可被计划器裁剪到单个 chunk 时SINGLE_SHARDexplain 顶层不再有limitAmount/skipAmount需要进shards[0]查看分片侧统计纯skip无 limit的多分片 count 无法截断分片侧扫描场景 2 中 9 个键全部被读这是分片 count 跳过分页时最需要警惕的代价模式由于requires_fcv_82标签上述limit/skip 跟踪与nCounted 为合并总数的行为断言以 FCV 8.2 为适用前提在更低兼容级别下运行同一查询时explain 的字段分布可能不同引用这些输出时应先确认集群的 featureCompatibilityVersion。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考