ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MongoDB join 优化:base collection 重新排序(base_coll_reorder)Golden 测试深度解析

MongoDB join 优化:base collection 重新排序(base_coll_reorder)Golden 测试深度解析 MongoDB join 优化base collection 重新排序base_coll_reorderGolden 测试深度解析【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文基于 MongoDB 源码仓库中jstests/query_golden/expected_output/internalEnableJoinOptimization/base_coll_reorder.md这份 golden 测试预期输出文件结合其驱动脚本jstests/query_golden/join_opt/base_coll_reorder_md.js系统讲解 MongoDBinternalEnableJoinOptimizationjoin 优化框架下base collection基集合在连接图join graph中的重新排序行为。读完本文你将掌握 join 优化涉及的三个关键服务器参数、golden 测试的期望输出格式以及 35 节点连接图在不同随机种子下生成的各类执行计划树HASH_JOIN_EMBEDDING / NESTED_LOOP_JOIN_EMBEDDING应如何解读。一、背景什么是 base collection 重新排序在 MongoDB 的查询优化器中当一个聚合管道包含多个$lookup$unwind即隐式 JOIN时优化器可以构建一张join graph连接图图中的每个节点代表一个表base collection 或 lookup 的外表节点间的边代表可执行的连接谓词。join 优化的核心任务之一就是决定图中各节点的连接顺序join order与连接方法join method以最小化执行成本。base collection reorder 特指最初的集合管道起点的集合即db.collection本身对应的节点在 join 树中被放置的位置。由于 base collection 没有$lookup的语义束缚理论上它可以出现在连接树的任意位置——这正是本测试想要验证的核心行为在随机重排模式下base collection 节点能否被合法地移动到连接树的不同层级且最终查询结果与不开启 join 优化时完全一致。实现层面这一逻辑位于src/mongo/db/query/compiler/optimizer/join/目录下涉及join_graph.h连接图构建、reorder_joins.h连接重排、join_plan.h连接计划与join_method.h连接方法选择等模块参数internalJoinReorderMode的字符串解析实现在src/mongo/db/query/join_reorder_mode.cpp中JoinReorderMode::setFromString。二、测试脚本结构六类连接图场景驱动脚本jstests/query_golden/join_opt/base_coll_reorder_md.js通过joinTestWrapper包装运行共覆盖 6 个 section对应 6 种不同的连接图拓扑。每个 section 调用runRandomReorderTests(pipeline)先关闭 join 优化跑一次基线No join opt再以随机重排模式枚举 seed 011 逐一运行。Section图规模图特征关键 pipeline 元素13 节点base 节点全连接A - BASE - B两个$lookup分别基于a、b字段23 节点base 节点仅连接一个节点BASE - A - B第二个 lookup 的localField为x.b依赖第一个 lookup 的结果33 节点存在潜在可推断边BASE↔A、BASE↔BA 与 B 之间可推断两个 lookup 均以base为连接字段43 节点中间含排除投影exclusion projection与字段重命名$project将a重命名为m、b重命名为zlookup 子管道内含投影54 节点潜在可推断边 过滤器$match: {b: {$eq: 3}} 三个 lookup65 节点多过滤器$match 四个带子管道$match的 lookup以第 6 个 section 为例其 pipeline 由 1 个$match和 4 个$lookup$unwind组成lookup 分别关联集合 a、b、base、bas 名分别为aaa、bbb、ccc、ddd并在最外层投影剔除所有嵌入文档的_id字段{$match: {b: {$eq: 3}}}, {$lookup: {from: a.getName(), as: aaa, localField: a, foreignField: a, pipeline: [{$match: {base: {$in: [22, 33]}}}]}}, {$unwind: $aaa}, {$lookup: {from: b.getName(), as: bbb, localField: b, foreignField: b, pipeline: [{$match: {base: {$gt: 20}}}]}}, {$unwind: $bbb}, {$lookup: {from: coll.getName(), as: ccc, localField: aaa.base, foreignField: base, pipeline: [{$match: {b: {$lt: 0}}}]}}, {$unwind: $ccc}, {$lookup: {from: b.getName(), as: ddd, localField: base, foreignField: base, pipeline: [{$match: {b: {$gt: 0}}}]}}, {$unwind: $ddd}, {$project: {_id: 0, aaa._id: 0, bbb._id: 0, ccc._id: 0, ddd._id: 0}}注意这里的集合命名测试通过db[jsTestName() _base]、_a、_b分别创建base、a、b三个集合jsTestName()即base_coll_reorder_md与预期输出文件base_coll_reorder.md一一对应因此预期输出中会出现test.base_coll_reorder_md_base、test.base_coll_reorder_md_a、test.base_coll_reorder_md_b等集合名。每个集合插入 45 条文档并各创建一个复合索引{dummy: 1, base: -1, a: 1, b: 1}。脚本注释说明创建该索引是为了提供multikeyness路径数组性信息使优化器在连接图构建阶段能正确判断各路径是否为数组路径——这是 join 优化合法性检查的一部分。三、核心服务器参数开启与随机重排base_coll_reorder_md.js通过db.adminCommand({setParameter: 1, ...})动态设置三个核心参数internalEnableJoinOptimization全局开关。为false时走传统计划golden 输出中的 No join opt 基线为true时启用 join 优化。internalJoinReorderMode: random连接重排模式设为随机。src/mongo/db/query/join_reorder_mode.cpp中JoinReorderMode::setFromString将字符串反序列化为JoinReorderModeEnum供重排器消费。internalRandomJoinOrderSeed随机种子测试从 0 递增到 11。joinTestWrapper定义于jstests/libs/query/join_utils.js在测试前通过getParameter快照全部 join 相关参数除上述外还包括internalJoinPlanTreeShape、internalMaxNodesInJoinGraph、internalMaxEdgesInJoinGraph、internalMaxNumberNodesConsideredForImplicitEdges、internalJoinPlanSamplingSize、internalJoinEnumerateCollScanPlans、internalMinAllPlansEnumerationSubsetLevel、internalMaxAllPlansEnumerationSubsetLevel、internalJoinOptimizationSamplingCEMethod、internalJoinMethod测试结束后统一恢复保证测试间的隔离。该 wrapper 还会为aggregate/explain挂上fsync钩子hookFsyncForJoinOpt确保 join 优化运行时磁盘状态稳定。种子去重机制runRandomReorderTests维护一个seen集合对每个 seed 先调用getWinningJoinOrderOneLine(explain)实现在jstests/query_golden/libs/pretty_plan.js将 winning plan 压缩成单行缩写如(HJ _ COLLSCAN [...], _ (NLJ ...))其中HJ/NLJ/INLJ分别代表 HASH_JOIN_EMBEDDING / NESTED_LOOP_JOIN_EMBEDDING / INDEXED_NESTED_LOOP_JOIN_EMBEDDING缩写映射见join_utils.js的joinStageAbbreviation。若该 join order 已出现过则跳过因此预期输出中没有 seed 4/7 等并非遗漏而是这些 seed 生成的 join order 与之前某个 seed 重复。结果等价性校验只有当某 seed 的 join order 是首次出现时脚本才将随机重排的聚合结果与 No join opt 基线做_resultSetsEqualUnordered无序集合相等断言保证任何重排都不改变查询语义结果。这正是base collection 可以自由重排这一优化合法性的实证。四、预期输出格式执行计划树详解Golden 测试通过jstests/libs/query/pretty_md.js的section/subSection/code将 explain 的 winning plan 以树状形式写入 Markdown 预期文件。pretty_plan.js的prettyPrintPlan使用prettyPrintTree渲染每个节点的输出行包括stage 名 集合名如HASH_JOIN_EMBEDDING [a a]、COLLSCAN [test.base_coll_reorder_md_a]。[a a]是joinPredicates的展示连接谓词。leftEmbeddingField/rightEmbeddingField左右子树的嵌入字段。none表示该侧不通过嵌入字段访问如 base 侧或谓词重命名后的结果。direction: forwardCOLLSCAN 的扫描方向。filter: {...}下推到扫描节点的过滤谓词如{ b : { $eq : 3 } }、{ $and : [...] }。pretty_plan.js会省略空 filter。PROJECTION_DEFAULT/PROJECTION_SIMPLE投影节点transformBy给出投影规则。PROJECTION_SIMPLE仅做字段排除如{ _id : false }PROJECTION_DEFAULT处理包含表达式变换的投影如{ m : $a, z : $b, _id : false }。树的层级关系用|竖线表达父子嵌套缩进越深表示节点在 join 树中的层级越靠下先执行。五、逐场景解读base collection 的移动规律场景 13-Node graphbase 节点全连接A - BASE - B管道为$lookup(a)→$unwind(x)→$lookup(b)→$unwind(y)即自然书写顺序是 base → A → B但 base 与 A、B 均存在直接连接边。seed 0 的输出显示 base 节点被移到了树的最深层HASH_JOIN_EMBEDDING [a a] leftEmbeddingField: none rightEmbeddingField: x | | | COLLSCAN [test.base_coll_reorder_md_a] | direction: forward | HASH_JOIN_EMBEDDING [b b] leftEmbeddingField: none rightEmbeddingField: y | | | COLLSCAN [test.base_coll_reorder_md_b] | direction: forward | COLLSCAN [test.base_coll_reorder_md_base] direction: forward注意左右子树的位置左子树是 A 的 COLLSCAN右子树是嵌套的 (B ⟕ base) 连接——base collection 被放到了最内层、最先执行与管道书写顺序base 在最外层完全相反。join 优化器证明这种翻转是语义等价的。seed 2 则生成了另一棵形状完全不同的树——base 被嵌套在 A 与 B 之间且所有连接方法均为 HASH_JOIN_EMBEDDINGHASH_JOIN_EMBEDDING [b b] leftEmbeddingField: y rightEmbeddingField: none | | | HASH_JOIN_EMBEDDING [a a] | leftEmbeddingField: x | rightEmbeddingField: none | | | | | COLLSCAN [test.base_coll_reorder_md_base] | | direction: forward | | | COLLSCAN [test.base_coll_reorder_md_a] | direction: forward | COLLSCAN [test.base_coll_reorder_md_b] direction: forwardseed 3 / 5 / 8 / 10 / 11 则展示了混合连接方法同一棵树中同时出现NESTED_LOOP_JOIN_EMBEDDING与HASH_JOIN_EMBEDDING。例如 seed 3 中外层为 HASH_JOINb 侧内层为 NESTED_LOOP_JOINa 侧seed 6 中 HASH_JOIN 在外层、NESTED_LOOP_JOIN 在内层且 base 位于最底层。这说明随机重排模式下连接顺序与连接方法的选择相互独立同一图拓扑可对应多种合法执行计划。场景 23-Node graphbase 节点仅连接一个节点BASE - A - B管道第二个 lookup 使用localField: x.b即B 只能与 A的外键相连不能直接与 base 相连。此时连接图呈链式 BASE-A-Bbase 只与 A 有边。输出中 base 节点总是出现在与 A 的连接附近且HASH_JOIN_EMBEDDING [x.a a]/[a x.a]这类谓词体现了跨嵌入字段x.a的连接——leftEmbeddingField/rightEmbeddingField组合随 base 与 A 的相对位置变化而翻转。例如 seed 0HASH_JOIN_EMBEDDING [x.a a] leftEmbeddingField: none rightEmbeddingField: none | | | COLLSCAN [test.base_coll_reorder_md_base] | direction: forward | HASH_JOIN_EMBEDDING [b b] leftEmbeddingField: x rightEmbeddingField: y | | | COLLSCAN [test.base_coll_reorder_md_b] | direction: forward | COLLSCAN [test.base_coll_reorder_md_a] direction: forward这里 base 位于最外层最先输出A 在最内层——同样与管道书写顺序相反且连接谓词[x.a a]表明 base 侧字段出现在嵌入文档x中。场景 33-Node graph potentially inferred edge潜在可推断边两个 lookup 均以base为连接字段BASE↔A、BASE↔B。由于 A 与 B 共享base字段A 与 B 之间可以通过传递性推断出潜在边potentially inferred edge。输出中出现了三路谓词如 seed 0 的HASH_JOIN_EMBEDDING [base base,y.base base]即一个节点同时携带两个连接谓词对 base 的直接连接 通过y.base的推断连接。这种多谓词连接是推断边被成功物化的直接证据。internalMaxNumberNodesConsideredForImplicitEdges等参数见join_utils.js的参数快照列表正用于限制隐式边推断的搜索规模。场景 43-Node graph intermediate exclusion projection rename管道首先执行{$project: {_id: 0, m: $a, z: $b}}将 base 的a/b字段重命名为m/z并排除_id两个 lookup 的localField使用新名字m、z且 lookup 子管道各带投影{x: $a}与{_id: 0}。输出中出现PROJECTION_DEFAULT/PROJECTION_SIMPLE节点PROJECTION_DEFAULT transformBy: { m : $a, z : $b, _id : false }seed 2 展示了投影与重排的复杂组合——HASH_JOIN_EMBEDDING [b z]外层leftEmbeddingField: y下嵌套HASH_JOIN_EMBEDDING [x m]内层三个投影节点分别贴附在对应 COLLSCAN 之上。该场景验证join 优化不仅能重排集合节点还能跨越中间的投影/重命名节点正确维护字段映射关系transformBy: { x : $a, _id : false }表明a字段被投影为x。场景 54-Node graph potentially inferred edges filters管道在 base 上先做$match: {b: {$eq: 3}}并有一个自关联 lookupfrom: coll.getName()即 base 表自身。输出中的 COLLSCAN 节点带上了filter字段如filter: { base : { $gt : 3 } }、filter: { b : { $eq : 3 } }。可以观察到过滤器跟随对应集合节点一起在 join 树中移动无论节点如何重排filter 始终粘附在其所属集合的 COLLSCAN 上。seed 3 的树中两个带 filter 的 base 扫描被拆分到树的顶层与底层说明优化器为同一集合多次出现保留了多个独立节点各自携带各自的 filter。场景 65-Node graph filters最复杂的场景5 个节点base 出现 2 次、4 个连接、3 个 lookup 子管道 filter 加 1 个管道级 filter。连接谓词形态更加多样[aaa.base base]通过嵌入文档字段连接、[a aaa.a]外键在嵌入文档侧。seed 0 的树为纯左深left-deep结构——每个 join 节点的右子树都是叶子 COLLSCANseed 5 / 9 / 11 则出现多棵 b 集合的独立扫描base_coll_reorder_md_b出现多次各带不同 filter表明优化器按 filter 差异为同一集合建立了多个图节点。所有输出中每个 COLLSCAN 的 filter 均与base_coll_reorder_md.js中管道定义的匹配条件一一对应{ b : { $eq : 3 } }←$match: {b: {$eq: 3}}{ base : { $in : [ 22, 33 ] } }← lookupaaa的子管道{ $and : [ { b : { $eq : 3 } }, { base : { $gt : 20 } } ] }← lookupbbb的子管道{ $and : [ { b : { $lt : 0 } }, { base : { $in : [ 22, 33 ] } } ] }← lookupccc的子管道{ b : { $gt : 0 } }← lookupddd的子管道六、随机重排的确定性与覆盖策略base_coll_reorder.md共包含 6 个 section × 最多 12 个 seed 的枚举但许多 seed 因 join order 重复被跳过如场景 1 仅保留 seed 0/1/2/3/5/6/8/10/11。这种设计带来两点收益确定性回归internalRandomJoinOrderSeed固定后重排结果是确定性的golden 输出可以稳定复现作为 join 优化器的回归基线。覆盖多样性虽然模式是随机的但通过多 seed 枚举 去重测试在有限输出空间内覆盖了左深树、右深树、混合连接方法、多谓词连接、投影穿越等关键计划形态。测试脚本对每个 section 的 No join opt 基线也调用runSingleTest(..., false)seen传false但因基线下不产生 join 节点prettyPrintWinningPlan实际不输出计划树故文档中 No join opt 小节后直接跟随各 seed 的结果。七、与源码的印证参数解析internalJoinReorderMode的random取值在src/mongo/db/query/join_reorder_mode.cpp中经idl::deserializeJoinReorderModeEnum完成字符串到枚举的转换属于查询集成旋钮query_integration_knobs_gen.h之一。重排器与连接图src/mongo/db/query/compiler/optimizer/join/下的reorder_joins.cpp重排算法、join_graph.cpp连接图与潜在推断边的构建、join_plan.cpp连接计划选择共同构成本节目的实现主体join_method.cpp负责HASH_JOIN_EMBEDDING与NESTED_LOOP_JOIN_EMBEDDING等方法的选择。代价与基数估计join_cost_estimator_impl.cpp与cardinality_estimator_join_test.cpp支撑重排时对每个候选顺序的代价比较这也解释了为何不同 seed 会得到不同但都合法的计划。测试断言辅助jstests/libs/query/join_utils.js提供的usedHashJoinEmbedding、usedNestedLoopJoinEmbedding、usedIndexedNestLoopJoinEmbedding、assertAllJoinsUseMethod等函数可在其他 join 优化测试中校验 winning plan 是否恰好包含期望的连接方法。八、如何运行与扩展该测试该测试属于golden 测试体系运行测试时printGolden来自pretty_md.js将当前输出写入预期文件测试框架对比其与jstests/query_golden/expected_output/internalEnableJoinOptimization/base_coll_reorder.md的一致性。测试标签要求requires_fcv_90Feature Compatibility Version 9.0与requires_sbeSBE 执行引擎可用 resmoke 或 Bazel 目标执行仓库中对应的驱动脚本位于jstests/query_golden/join_opt/base_coll_reorder_md.js。如需扩展覆盖场景可参照runRandomReorderTests的模式新增一个section(...)构造新的 pipeline例如引入$group、$sort或带let/pipeline的关联子查询随后沿用关闭优化跑基线 → 随机种子枚举 → 结果等价断言三步曲。若希望固定某种连接方法可结合internalJoinMethod参数或join_utils.js中的断言辅助函数验证计划形状。九、小结base_coll_reorder.md这份 golden 输出以 100 棵执行计划树实证了 MongoDB join 优化的一个关键能力base collection 节点可以在 join 图中自由重排并跨越投影、重命名、过滤器等中间算子同时保证查询结果语义不变。读懂这份输出等于掌握了一套阅读 join 优化执行计划的标准姿势——连接谓词[a a]、嵌入字段leftEmbeddingField/rightEmbeddingField、扫描过滤filter与投影transformBy四要素是理解 MongoDB 内部 join 计划的核心语言。延伸阅读测试驱动脚本base_coll_reorder_md.jsGolden 输出目录下的同类测试basic_joins.md、join_cardinality_estimation.md、null_semantics.md计划树美化与单行缩写pretty_plan.js、join_utils.jsGolden 输出基础设施pretty_md.jsJoin 优化核心源码join含reorder_joins.cpp、join_graph.cpp、join_plan.cpp、join_method.cpp重排模式参数解析join_reorder_mode.cpp【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表