
MongoDB 分片集群中带 limit/skip 的 find 查询 Explain 实战解析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读本文基于 MongoDB 开源仓库jstests/query_golden_sharding/sharded_explain_find_with_limit_skip.js及其 golden 期望输出文档系统讲解分片集群场景下带limit、skip、sort的 find 查询在 mongos 路由器上的 explain 输出结构。你将掌握SHARD_MERGE_SORT与SINGLE_SHARD两种路由器执行形态的区别、limitAmount/skipAmount在分片与路由器两级的正确语义以及如何用 explain 的nReturned判断结果少于 limit的真实返回情况并理解 golden 测试框架如何固化这类行为。一、背景与测试目标在分片集群中执行带limit/skip的排序查询时mongos路由器需要决定把limit/skip下推给各分片多少、在路由器自身保留多少。如果语义处理不当会出现返回条数不足跳过结果错误等 bug。该 golden 测试的用途正是验证路由器上的分片 explain 能否正确追踪 limit 与 skip 值并确认nReturned反映的是各分片合并后的总数而不是各分片返回值的简单加总。测试用例位于 sharded_explain_find_with_limit_skip.js其注释明确写到Tests that sharded explain for find commands correctly tracks limit and skip values on the router when targeting multiple shards. Also verifies that nReturned reflects the merged total, not the sum across shards.该测试的另一个价值是同时覆盖 classic 与 SBESlot-Based Execution两种执行引擎在 sbeRestricted默认 classic 路径与 sbeFullSBE 全量开启下分别记录期望输出用于锁定不同引擎下 explain 的稳定行为。二、测试环境与数据分布测试通过ShardingTest搭建 1 个 mongos、2 个分片、1 个 config server 的最小分片集群const st new ShardingTest({mongos: 1, shards: 2, config: 1});关键步骤对应 sharded_explain_find_with_limit_skip.js启用分片并指定片键以{x: 1}作为片键调用shardCollection写入 100 条数据文档形如{_id: i, x: i}i从 0 到 99在x: 50处 split 成两个 chunk将低 chunkx: 45迁移到 shard0高 chunkx: 55迁移到 shard1。最终数据分布为shard0 持有x ∈ [0, 49]shard1 持有x ∈ [50, 99]。集合上只有两个索引_id_与x_1explain 输出中的 Total indexes on the collection 一节即来自 golden_test_utils.js 的outputAvailableIndexes辅助函数。查询通过统一的辅助函数runFindAndExplain组装游标并输出 golden 结果sharded_explain_find_with_limit_skip.jsfunction runFindAndExplain({query, options {}, expected {}}) { let cursor coll.find(query); if (options.sort) cursor cursor.sort(options.sort); if (options.skip ! undefined) cursor cursor.skip(options.skip); if (options.limit ! undefined) cursor cursor.limit(options.limit); outputFindPlanAndResults(coll, cursor, expected); }expected参数会断言路由器顶层执行阶段的stageSHARD_MERGE_SORT或SINGLE_SHARD以及limitAmount/skipAmount的取值这些断言逻辑定义在 golden_test_utils.js 的outputCommonPlanAndResults中assert.eq(executionStages.stage, expected.stage); if (expected.limit ! undefined) { assert.eq(executionStages.limitAmount, expected.limit); } if (expected.skip ! undefined) { assert.eq(executionStages.skipAmount, expected.skip); }三、8 个测试用例速览该 golden 测试共 8 个用例覆盖两个维度多分片定位 vs 单分片定位×limit / skip / limitskip / 结果不足。#用例名过滤条件limitskip路由器 stage返回文档1Simple limit targeting multiple shardsx 305—SHARD_MERGE_SORTx 31..352Simple skip targeting multiple shards45 x 55—5SHARD_MERGE_SORTx 51..543Limit skip targeting multiple shardsx 3055SHARD_MERGE_SORTx 36..404nReturned lower than limit49 ≤ x ≤ 515—SHARD_MERGE_SORTx 49..513 条5nReturned lower than skip limit47 ≤ x ≤ 5555SHARD_MERGE_SORTx 52..554 条6Simple limit targeting single shardx 905—SINGLE_SHARDx 91..957Simple skip targeting single shardx 90—5SINGLE_SHARDx 96..998Simple limit skip targeting single shardx 9055SINGLE_SHARDx 96..99完整的查询 JSON、结果与 explain 输出见 sbeRestricted/sharded_explain_find_with_limit_skip.md下文逐一分析关键用例。四、多分片定位SHARD_MERGE_SORT 形态4.1 用例 1Simple limit targeting multiple shards查询x 30limit: 5按x升序排序singleBatch: false允许分片分批返回{ find: sharded_explain_find_with_limit_skip, filter: {x: {$gt: 30}}, limit: 5, singleBatch: false, sort: {x: 1} }由于x 30跨越了 shard00–49与 shard150–99两个分片路由器需要在两级执行 limit每个分片本地执行limit: 5路由器在SHARD_MERGE_SORT之上再施加limitAmount: 5。分片内执行计划classic 引擎IXSCAN(5) → SHARDING_FILTER(5) → FETCH(5) → SORT_KEY_GENERATOR(5) → PROJECTION_DEFAULT(5) → LIMIT(limitAmount: 5, nReturned: 5)路由器顶层{ stage: SHARD_MERGE_SORT, limitAmount: 5, nReturned: 5, shards: [ ...两个分片各 nReturned: 5... ], totalDocsExamined: 10, totalKeysExamined: 10 }这里有两个值得注意的语义分片上的limitAmount: 5不等于路由器上的limitAmount: 5表示同一个东西分片上的 LIMIT 是下推的本地上限路由器上的limitAmount是最终要返回条数。两者恰好都是 5是因为排序字段与片键一致且数据分布均匀路由器选择了每分片 5、合并取 5的下推策略totalDocsExamined: 10每分片 5与totalKeysExamined: 10表明虽然最终只返回 5 条但两个分片各扫描并读取了 5 个文档详见 sbeRestricted/sharded_explain_find_with_limit_skip.md。4.2 用例 2Simple skip targeting multiple shards查询45 x 55skip: 5按x升序排序。数据分布在 shard046–494 条与 shard150–545 条。分片内执行计划为IXSCAN → SHARDING_FILTER → FETCH → SORT_KEY_GENERATOR → PROJECTION_DEFAULT注意分片内没有 SKIP 阶段skip 完全由路由器承担路由器顶层为{ stage: SHARD_MERGE_SORT, skipAmount: 5, nReturned: 4, shards: [ {nReturned: 4}, {nReturned: 5} ], totalDocsExamined: 9, totalKeysExamined: 9 }合并后按x排序为 46, 47, 48, 49, 50, 51, 52, 53, 54跳过前 5 条后剩下 51, 52, 53, 54nReturned: 4与测试结果的 4 条文档完全一致。这里体现出skip 与 limit 的下推策略不同skip 只作用于路由器而 limit 会按需下推给各分片详见 sbeRestricted/sharded_explain_find_with_limit_skip.md。4.3 用例 3Limit skip targeting multiple shards查询x 30skip: 5, limit: 5。为了让路由器最终能跳过 5 条后再取 5 条分片本地需要把 limit 放大为limit: 10IXSCAN(10) → SHARDING_FILTER(10) → FETCH(10) → SORT_KEY_GENERATOR(10) → PROJECTION_DEFAULT(10) → LIMIT(limitAmount: 10, nReturned: 10)路由器顶层同时出现两个字段{ stage: SHARD_MERGE_SORT, limitAmount: 5, skipAmount: 5, nReturned: 5, shards: [ {nReturned: 10}, {nReturned: 10} ], totalDocsExamined: 20, totalKeysExamined: 20 }这正是本测试想验证的核心行为之一skip limit组合时分片上的 LIMIT 值10 skip limit与路由器上的 LIMIT 值5不同explain 必须分别正确记录两者的limitAmount详见 sbeRestricted/sharded_explain_find_with_limit_skip.md。4.4 用例 4 与 5结果不足时 nReturned 反映真实数量用例 449 ≤ x ≤ 51只有 3 条命中虽然limit: 5最终返回 3 条。路由器limitAmount: 5但nReturned: 3两个分片分别返回 1 条与 2 条详见输出用例 547 ≤ x ≤ 55共 9 条命中skip: 5, limit: 5后返回 4 条52, 53, 54, 55。分片本地limitAmount: 10shard0 返回 3 条、shard1 返回 6 条路由器合并后跳过 5 条再限 5 条实际仅剩 4 条详见输出。结论nReturned永远是实际返回给客户端的条数不会因为设置了 limit 就虚报为 limit 值。这也是 golden_test_utils.js 中outputFindPlanAndResults的硬性断言所保证的const actualReturned results.length; assert.eq(actualReturned, explain.executionStats.nReturned); assert.eq(actualReturned, executionStages.nReturned);五、单分片定位SINGLE_SHARD 形态当查询条件x 90使 mongos 可以确定目标数据只存在于单个分片时路由器不再做合并排序顶层阶段变为SINGLE_SHARD分片执行计划原样上抛不产生limitAmount/skipAmount聚合字段如果原样计划中没有的话。5.1 用例 6Simple limit targeting single shard分片内 classic 执行计划IXSCAN(5) → SHARDING_FILTER(5) → FETCH(5) → LIMIT(limitAmount: 5)路由器顶层仅stage: SINGLE_SHARDtotalDocsExamined: 5、totalKeysExamined: 5与分片一致详见输出。5.2 用例 7Simple skip targeting single shard分片内执行计划出现SKIP 阶段IXSCAN(9) → SHARDING_FILTER(9) → SKIP(skipAmount: 5, nReturned: 4) → FETCH(4)注意这里totalKeysExamined: 9而totalDocsExamined: 4跳过操作只扫描索引键9 个键并不读取文档被跳过的文档不计入 totalDocsExamined。这与多分片用例 2 中 skip 只在路由器执行的策略形成了鲜明对比——单分片定位时 mongos 可以把 skip 完整下推从而让分片用IXSCAN SKIP的高效路径直接跳过详见输出。5.3 用例 8Simple limit skip targeting single shardSKIP 与 LIMIT 同时下推IXSCAN(9) → SHARDING_FILTER(9) → SKIP(skipAmount: 5, nReturned: 4) → FETCH(4) → LIMIT(limitAmount: 5, nReturned: 4)最终返回 96, 97, 98, 99 共 4 条详见输出。5.4 单分片 vs 多分片下推策略小结场景路由器 stageskip 处理limit 处理多分片 limitSHARD_MERGE_SORT不下推下推limitskiplimit 时下推skiplimit多分片 skipSHARD_MERGE_SORT仅在路由器无单分片 limit/skipSINGLE_SHARD完整下推SKIP阶段完整下推LIMIT阶段六、classic 与 SBE 引擎的 explain 差异该 golden 测试在多个 feature flag 变体下记录了期望输出。对比 sbeRestrictedclassic与 sbeFullSBE可以看到classic 引擎阶段名为大写形式IXSCAN、SHARDING_FILTER、FETCH、SORT_KEY_GENERATOR、PROJECTION_DEFAULT、LIMIT、SKIPSBE 引擎阶段名变为小写ixseek、filter、fetch、limit、limitskip且执行计划更紧凑。例如单分片 limit 用例在 SBE 下为ixseek → filter → fetch → limitsbeFull 用例 6单分片 skip 用例为ixseek → filter → limitskip → fetchlimitskip 为ixseek → filter → limitskip → fetch → limit引擎标识多分片场景下explain 输出不再单独打印 Execution Engine 行因为各分片可能使用不同引擎而单分片场景会明确打印Execution Engine: classic或Execution Engine: sbe。SBE 的limit_skip阶段实现位于 src/mongo/db/exec/sbe/stages/limit_skip.cppclassic 的 LIMIT/SKIP 阶段分别位于 src/mongo/db/exec/classic/limit.cpp 与 src/mongo/db/exec/classic/skip.cpp。以 classic 的 LimitStage 为例其构造参数直接携带 limit 值并写入_specificStats.limit对应 explain 中的limitAmountSkipStage 则记录_leftToSkip与_skipAmount。从 skip.cpp 的doWork实现可以看到跳过语义_leftToSkip 0时调用_ws-free(id)直接丢弃文档而不返回这正是跳过只扫键不读文档、totalDocsExamined 不增长的底层原因。七、如何阅读与复现golden 测试的运行机制7.1 输出文件是如何生成的该.md文件并非手写而是由测试运行时通过 golden 框架自动生成。pretty_md.js中的 section/subSection/code 等辅助函数 会把## N.section、###subSection与代码块逐行写入文件配合printGolden输出golden 框架的具体机制详见 docs/golden_data_test_framework.md测试输出与已检入的期望输出比对任何差异都会导致测试失败需要同步更新代码或期望输出。7.2 复现步骤在编译好的 MongoDB 源码树中通过 resmoke 运行该 golden 测试# 使用 resmoke 运行测试 python buildscripts/resmoke.py run --suite jstests/query_golden_sharding --shell jstests/query_golden_sharding/sharded_explain_find_with_limit_skip.js或通过 Bazel 运行BUILD.bazel 已把所有 js 测试文件聚合为all_javascript_files库bazel test //jstests/query_golden_sharding:all_javascript_files注意该测试带有requires_fcv_82标签需要 FCV 82 及以上的服务器版本assumes_read_concern_local表示测试假定默认 read concern 为 local详见 sharded_explain_find_with_limit_skip.js。7.3 期望输出文件的分层结构期望输出按引擎与 feature flag 变体存放于jstests/query_golden_sharding/expected_output/目录sbeRestricted/——SBE 受限模式本关联文档所在目录classic 为主sbeFull/——SBE 全量开启sbeDisabled/——SBE 完全关闭classicfeatureFlagSbeFull/——通过 feature flag 开启 SBE full 的对照变体。各变体的用例 1–8 查询与结果完全一致差异仅体现在执行引擎与阶段命名上便于对照验证不同引擎下 limit/skip 语义的一致性。八、与 count 命令的横向对照同目录下还有一个姊妹测试 sharded_explain_count_with_limit_skip.js期望输出见 sharded_explain_count_with_limit_skip.md它验证 count 命令带 limit/skip 时的行为count 场景用nCounted代替nReturnedCOUNT阶段不返回文档因此totalDocsExamined: 0但totalKeysExamined与 find 一致如用例 1 中每分片扫 5 个键多分片 count 用SHARD_MERGE而非SHARD_MERGE_SORT聚合路由器的limitAmount/skipAmount语义与 find 相同单分片 count 用例 8 中分片COUNT阶段在 skip 后计数 5验证skip limit下 count 的取值为 5 而不是 10。它由 golden_test_utils.js 的outputCountPlanAndResults驱动并断言actualCount executionStages.nCounted。通过对照可以发现limit/skip 的路由器追踪逻辑对 find 与 count 是统一的只是计数字段不同nReturned vs nCounted。九、实践要点总结读 explain 先看路由器顶层 stageSHARD_MERGE_SORT表示多分片合并排序SINGLE_SHARD表示单分片直通顶层shards数组内才是各分片的真实执行计划。区分两个limitAmount分片内的limitAmount是下推的本地上限skip limit组合下会变成skip limit路由器顶层的limitAmount才是最终返回条数。nReturned不等于 limit结果不足时nReturned反映真实返回数量用它判断有没有取满比猜 limit 更可靠。skip 的下推策略取决于定位单分片定位时 skip 会下推为分片内的SKIP/limitskip阶段只扫键不读文档totalDocsExamined不增长多分片定位时 skip 仅在路由器执行。引擎差异一眼可辨classic 用大写阶段名IXSCAN/FETCH/LIMITSBE 用小写ixseek/fetch/limit单分片 explain 会打印Execution Engine行多分片不打印。golden 文件是行为契约修改查询执行、下推策略或 explain 输出格式时必须同步更新 expected_output 下各变体的期望文件并通过 golden 比对确认行为符合预期。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考