十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

MongoDB 聚合管道 Join 优化(Join Ordering)Golden Test 全解析:从 $lookup 到 Join 计划枚举

MongoDB 聚合管道 Join 优化(Join Ordering)Golden Test 全解析:从 $lookup 到 Join 计划枚举 MongoDB 聚合管道 Join 优化Join OrderingGolden Test 全解析从 $lookup 到 Join 计划枚举【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读本文以 MongoDB 官方仓库中jstests/query_golden/expected_output/internalEnableJoinOptimization/basic_joins.md这份 Golden Test 基准输出文档为骨架系统讲解 MongoDB SBE 执行引擎中Join 优化Join Optimization / Join Ordering的核心机制。该文档是jstests/query_golden/join_opt/basic_joins_md.js测试脚本的权威期望输出覆盖 37 个真实聚合管道用例。读完本文你将掌握MongoDB 如何把$lookup$unwind重写为底层 Join 节点、internalEnableJoinOptimization等内部参数的开启与调优方式、left-deep / right-deep / zig-zag / 随机种子等计划枚举策略对执行计划的影响以及如何借助 Golden Test 与 explain 输出验证 Join 优化是否生效。一、背景什么是 MongoDB 的 Join 优化MongoDB 的聚合框架通过$lookup提供跨集合关联能力。当管道中连续出现$lookup紧跟$unwind的组合时查询优化器在 SBE——Slot-Based Execution 引擎上会将其识别为一组可连接的关系从而进入 Join Ordering 逻辑把$lookup重写为底层 Join 节点并枚举不同的连接顺序与连接方法选出代价最低的执行计划。这份basic_joins.md就是该特性在基础 join 场景下的回归测试基准Golden Test。它由jstests/query_golden/join_opt/basic_joins_md.js驱动生成该测试带有requires_fcv_90与requires_sbe两个 tag见 basic_joins_md.js说明该特性依赖 MongoDB 9.0 功能兼容版本FCV 90以及 SBE 引擎。1.1 参与 Join 优化的核心参数从测试脚本与测试辅助库可以看到Join 优化由一组setParameter开关控制完整列表见 join_utils.js 与 query_knob_descriptors_optimization.h参数取值示例作用internalEnableJoinOptimizationtrue/false总开关关闭后走传统$lookup执行路径explain 中也不会出现usedJoinOptimization标志internalJoinReorderModebottomUp/randomJoin 顺序的搜索策略自底向上的完整枚举或随机顺序搜索internalJoinPlanTreeShapeleftDeep/rightDeep/zigZag限制计划树的形状左深、右深、任意树形internalRandomJoinOrderSeed44/45随机搜索的随机种子保证测试可复现internalMaxNodesInJoinGraph/internalMaxEdgesInJoinGraph数值限制 join 图规模internalJoinMethod等—控制允许使用的 join 方法等在plan_explainer_impl.cpp等 explain 输出代码中internalEnableJoinOptimization.load()直接决定是否在 winning plan 中输出usedJoinOptimization字段见 plan_explainer_impl.cpp。1.2 Golden Test 的验证逻辑每个测试用例都会先以internalEnableJoinOptimization: false运行一遍得到基准结果No join opt再在各种 join 优化参数组合下运行并断言执行结果与基准结果完全一致assertArrayEq({expected, actual})见 join_opt.jsexplain 输出中winningPlan.usedJoinOptimization truejoin_opt.js计划树里确实出现 join 优化节点NESTED_LOOP_JOIN_EMBEDDING/INDEXED_NESTED_LOOP_JOIN_EMBEDDING/HASH_JOIN_EMBEDDING判定函数见 join_utils.js。也就是说basic_joins.md中的每一段With ...小节都对应一次参数组合下的真实执行计划快照。二、测试数据与运行方式测试在test.basic_joins_md当前库下以测试名命名的集合上运行共有 4 张表// 基表 basic_joins_md5 条 {_id: 0, a: 1, b: foo}, {_id: 1, a: 1, b: bar}, {_id: 2, a: 2, b: bar}, {_id: 3, a: null, b: bar}, {_id: 4, b: bar}, // 无 a 字段 // foreign15 条通过 a 关联 {_id: 0, a: 1, c: zoo, d: 1}, {_id: 1, a: 2, c: blah, d: 2}, {_id: 2, a: 2, c: x, d: 3}, {_id: 3, a: null, c: x, d: 4}, {_id: 4, c: x, d: 5}, // 无 a 字段 // foreign23 条通过 b 关联 {_id: 0, b: bar, d: 2}, {_id: 1, b: bar, d: 6}, {_id: 2, b: baz, d: 7}, // foreign33 条通过 c 关联 {_id: 0, a: 1, c: zoo, d: 1}, {_id: 1, a: 2, c: blah, d: 2}, {_id: 2, a: 2, c: x, d: 3},关键细节每张表都额外创建了包含dummy字段的复合索引如{dummy: 1, a: -1, b: 1}注释明确说明这是为路径数组性multikeyness提供信息。另外在index join变体中会临时为 foreign1 建{a: 1}、为 foreign2 建{b: 1}索引测试结束即 drop见 basic_joins_md.js。运行入口是joinTestWrapperjoin_utils.js它在测试前保存全部 join 相关参数执行期间为aggregate/explain挂上fsync钩子以保证磁盘状态稳定join 代价估算依赖存储统计信息测试结束后恢复参数。三、计划枚举模式同一管道七种执行计划每个用例在开启 join 优化后测试脚本会依次用以下参数组合生成计划快照bottom-up left-deepinternalJoinReorderMode: bottomUpinternalJoinPlanTreeShape: leftDeepbottom-up right-deepinternalJoinPlanTreeShape: rightDeepzig-zaginternalJoinPlanTreeShape: zigZagrandom seed 44internalJoinReorderMode: randominternalRandomJoinOrderSeed: 44random seed 45同上seed 为 45random 索引internalJoinReorderMode: random同时 foreign1/foreign2 上有{a:1}/{b:1}索引bottom-up left-deep 索引以文档用例 1$lookup子管道含 3 个$match为例原始管道是[ {$lookup: { from: basic_joins_md_foreign1, as: x, localField: a, foreignField: a, pipeline: [ {$match: {d: {$lt: 3}}}, {$match: {c: blah}}, {$match: {_id: {$gt: 0}}} ] }}, {$unwind: $x} ]开启 join 优化后bottom-up left-deep / right-deep / zig-zag 三者的计划完全相同NESTED_LOOP_JOIN_EMBEDDING [a a] leftEmbeddingField: x rightEmbeddingField: none | | | COLLSCAN [test.basic_joins_md] | direction: forward | COLLSCAN [test.basic_joins_md_foreign1] filter: { $and : [ { c : { $eq : blah } }, { d : { $lt : 3 } }, { _id : { $gt : 0 } } ] } direction: forward这个计划揭示了两个重要机制多个$match被合并吸收子管道里的三个$match被合并且下推为 foreign 集合上的一个COLLSCAN filter$and形式说明 join 优化会先做谓词合并与下推再枚举 join 顺序NLJ 连接基表作为外层outerforeign 集合作为内层join 谓词是a a匹配结果通过leftEmbeddingField: x嵌入外层文档。而 random seed 44 下则选出了不同方法HASH_JOIN_EMBEDDING [a a] leftEmbeddingField: none rightEmbeddingField: x | | | COLLSCAN [test.basic_joins_md_foreign1] | filter: { $and : [ ... ] } | direction: forward | COLLSCAN [test.basic_joins_md] direction: forward注意这里连接方向反了过来foreign1 在左、基表在右且是哈希连接HJ。这直观展示了join ordering 的核心价值同一管道可以产生连接顺序与连接方法都不同的等价计划优化器选择其中之一。四、读懂 Join 节点方法、谓词与嵌入语义从全部 37 个用例的执行计划可以看出join 优化节点有清晰的输出约定对应辅助库中的节点识别逻辑 join_utils.js节点类型Join 方法HASH_JOIN_EMBEDDING——哈希连接测试中用HJ缩写表示NESTED_LOOP_JOIN_EMBEDDING——嵌套循环连接NLJINDEXED_NESTED_LOOP_JOIN_EMBEDDING——索引嵌套循环连接INLJ下方会挂FETCHINDEX_PROBE_NODE。谓词写法[a a]基于localField/foreignField的等值连接EQ lookup[a $ a]基于$expr的等值谓词见用例 15、25[x.a a]连接键引用了前面$lookup产出的字段见用例 8、9、10。嵌入语义leftEmbeddingField/rightEmbeddingField描述匹配结果嵌入到哪里值为x表示嵌入到该$lookup的as字段值为none则表示该侧结果被直接传递/展平不产生额外嵌套层级与$unwind语义对应。两个字段均为none出现在连接键涉及多层 lookup 引用如[x.a a]的场景用例 8、9。用例 93 个 join且后续 join 引用前面 lookup 的字段是一个很好的综合例子HASH_JOIN_EMBEDDING [x.c c] leftEmbeddingField: none rightEmbeddingField: z | | | COLLSCAN [test.basic_joins_md_foreign3] ... HASH_JOIN_EMBEDDING [a a] ... HASH_JOIN_EMBEDDING [b b] leftEmbeddingField: y ...最外层 join 谓词[x.c c]表明第三个$lookup的localField: x.c引用了第一个$lookup的结果字段x.c优化器需要在先算前两个 join、再算第三个 join的约束下枚举计划。这验证了 join 优化支持跨 lookup 的依赖关系建模。当该用例切换到 random 索引模式时文档记录了一个INDEXED_NESTED_LOOP_JOIN_EMBEDDING计划foreign2 上出现INDEX_PROBE_NODE [test.basic_joins_md_foreign2] keyPattern: { b : 1 } indexName: b_1 isMultiKey: false isUnique: false isSparse: false isPartial: false这正是测试脚本临时创建{b: 1}索引后优化器把 NLJ 升级为索引驱动的 INLJ 的直接证据。五、嵌套路径与 $exprjoin 键的复杂形态5.1 引用嵌套路径的 lookup用例 10用例 10 展示了as为嵌套路径时的处理as: w.y、as: k.y.z后续 lookup 再引用localField: w.y.d。其 bottom-up 计划中出现的[w.y.c c]、[d d]以及leftEmbeddingField: w.y、rightEmbeddingField: k.y.z说明嵌入目标字段本身可以是点分路径Join 节点能正确跟踪深层字段的归属关系。测试源码中有一段注释basic_joins_md.js提到 SERVER-113230说明该用例原本想测试更冲突的目标路径当前版本已调整为非冲突的w.y/k.y.z。5.2 $expr 谓词参与的 join用例 15当$lookup使用let 子管道$match: {$expr: {$eq: [$a, $$a]}}形式无 localField/foreignField时优化器同样能识别HASH_JOIN_EMBEDDING [a $ a] leftEmbeddingField: none rightEmbeddingField: x$记号即代表$expr等值谓词。在 random seed 45 与 index join 变体中还出现了[x.a $ a]与[b $ b]带INDEXED_NESTED_LOOP_JOIN_EMBEDDING证明$expr谓词同样可以被索引化。5.3 join 图中的环用例 14用例 14 是join 图存在环的经典场景三个$lookup的 foreignField 分别指向不同集合的_id且共享基表字段a。bottom-up 计划输出NESTED_LOOP_JOIN_EMBEDDING [a a, y._id a, z._id a]一个 join 节点上携带了多个等值谓词[a a, y._id a, z._id a]说明优化器会在连接顺序枚举中把共享键合并到同一层而不是机械地每个$lookup一个节点。5.4 无连接谓词的 lookup 也能参与 join用例 16用例 16 中第一个$lookup连 localField/foreignField 都没有pipeline: []但后续 lookup 的$expr同时引用了基表字段$a和该 lookup 产出的$coll12.a从而把三个集合间接连通。优化器识别出这种隐式连接图产出的计划形如HASH_JOIN_EMBEDDING [coll13.a $ a, a a] ... HASH_JOIN_EMBEDDING [a $ a] leftEmbeddingField: coll13 ...5.5 歧义字段投影用例 17用例 17 的$project: {_id: 0, d: 1}在基表和 foreign2 中都有字段d属于歧义字段。join 优化下的计划把投影下推为基表侧的PROJECTION_SIMPLEtransformBy: { a: true, d: true, _id: false }保证 join 谓词所需字段不被裁剪——这正是投影下推必须保留 join 谓词字段的约束体现。六、$match 的吸收语义as 字段过滤如何下推用例 1825 系统验证了对as字段joined 结果的$match会被吸收absorbed到 foreign 集合的扫描层用例 18单个$matchon as 字段管道为$lookup→$unwind→$match: {x.c: blah}优化后 foreign 集合的 COLLSCAN 出现filter: { c : { $eq : blah } }用例 19两个$match均在 as 字段上两个过滤条件合并为$andfilter: { $and : [ { c : { $eq : blah } }, { d : { $eq : 2 } } ] }用例 20as 字段$match后跟基表字段$match各自下推到对应集合——基表扫描带{ b : { $eq : bar } }foreign 扫描带{ c : { $eq : blah } }用例 21第二个 join 的 as 字段过滤$match: {y.d: {$gt: 2}}被吸收到 foreign2 的扫描层用例 22反例$match出现在$lookup之前时即使引用了 as 字段名此时尚未定义它也只能作为基表过滤器存在。计划中基表 COLLSCAN 的 filter 原样保留{ x.c : { $eq : blah } }而 foreign 集合没有任何过滤——测试名明确说明该过滤器不会被吸收进被 join 的集合用例 23/24/25分别验证pipeline: []、pipeline: [$match]、相关子管道correlated sub-pipeline三种形态下外层 as 字段$match依然可以被吸收sub-pipeline 自身谓词与吸收谓词合并且去重如用例 24 的$and: [{c: blah}, {d: {$lt: 3}}]。这一组用例在工程上是**谓词下推predicate pushdown**的核心保障join 优化绝不能因重写而丢失过滤语义且应尽可能把过滤提前到扫描阶段执行。七、投影下推PROJECTION_SIMPLE 与 PROJECTION_DEFAULT用例 1113、2631 集中验证 join 优化对$project的处理计划树中会出现两类投影节点PROJECTION_SIMPLE纯粹的字段保留/排除投影transformBy: {a: true, _id: false}形式可以直接下推叠加在 COLLSCAN 之上PROJECTION_DEFAULT包含表达式/常量/重命名的投影如transformBy: {_id: true, a: {$const: my-computed-field}, extra: $a}。几个代表性场景基表前缀$project用例 11、26、29$project位于$lookup之前优化器将其折叠到下推投影中如用例 29 的$project: {_id: 0, a: 1}变成基表 COLLSCAN 上的PROJECTION_SIMPLEtransformBy: { a : true, _id : false }。join 谓词字段a被保留其余字段被裁剪合成字段与重命名用例 12、13$project: {a: my-computed-field, extra: $a}生成PROJECTION_DEFAULTextra由$a表达式计算得出后续 lookup 以localField: extra关联最终 join 谓词显示为[x.a extra]——join 键可以建立在派生字段上sub-pipeline 内$project用例 27、28、30、31$lookup子管道里的$project被下推到 foreign 集合扫描之上如PROJECTION_SIMPLE叠加transformBy: {_id: false, c: false}同时只保留 join 谓词所需字段用例 27 保留a/b键字段投影绝不能裁剪 join 谓词字段用例 17 的歧义字段投影、用例 13 中$match: {$expr: {$eq: [$x.extra, $a]}}与PROJECTION_DEFAULT的配合都验证了这一约束。八、后缀 stage 的处理GROUP 之上的连接用例 37 在双 join 之后追加了$sortByCount内部实现为GROUP。开启 join 优化后计划树顶层出现GROUP节点其下仍是由多个HASH_JOIN_EMBEDDING组成的 join 子树。例如用例 3$sortByCount: $y.b结果为{_id: bar, count: 14}GROUP | HASH_JOIN_EMBEDDING [a a] ... HASH_JOIN_EMBEDDING [b b] ... PROJECTION_SIMPLE transformBy: { a : true, b : true, _id : false } | COLLSCAN [test.basic_joins_md]注意 bottom-up 计划中基表扫描上方出现了一个投影仅保留 join 与聚合所需的a/b字段而 random seed 45 变体中该投影被放到更靠近基表的位置。这说明suffix后缀stage 不影响 join 图构建但会影响投影/字段裁剪的摆放位置。用例 4、6 进一步叠加了 sub-pipeline 的未相关$match如 foreign1 子管道{$match: {d: {$lt: 3}}}、foreign2 子管道{$match: {b: {$gt: aaa}}}与管道前缀$match{$match: {a: {$gt: 1}}}对应 foreign 扫描上的filter: {d: {$lt: 3}}、filter: {b: {$gt: aaa}}以及基表扫描上的filter: {a: {$gt: 1}}。用例 6 的结果{_id: 2, count: 2}与用例 4 的{1: 2, 2: 2}形成对照展示前缀$match缩小基表输入后对聚合结果的影响。九、Join Hint用 $_internalJoinHint 强制 INLJ用例 3237 是 Golden Test 中非常工程化的部分它们通过管道首部的$_internalJoinHint显式指定 join 方法method: INLJ与左右子节点方向验证 hint 机制与投影/重命名的交互{ $_internalJoinHint: { perSubsetLevelMode: [ {level: NumberInt(0), mode: CHEAPEST}, {level: NumberInt(1), hint: {node: NumberInt(1), method: INLJ, isLeftChild: false}, mode: CHEAPEST} ] } }在 hint 生效的用例中注意测试脚本逻辑存在 hint 时只运行 zig-zag 一种枚举见 basic_joins_md.js计划形如INDEXED_NESTED_LOOP_JOIN_EMBEDDING [a a] leftEmbeddingField: none rightEmbeddingField: x | | | PROJECTION_DEFAULT | transformBy: { _id : true, a : true, computed : { $const : bar } } | | | FETCH [test.basic_joins_md] | | | | INDEX_PROBE_NODE [test.basic_joins_md] | keyPattern: { a : 1 } | indexName: a_1 ... PROJECTION_DEFAULT transformBy: { _id : true, a : true, computed : { $const : foo } } | COLLSCAN [test.basic_joins_md]要点FETCHINDEX_PROBE_NODEindexName: a_1表明 INLJ 通过索引探测完成逐行关联基表与子管道两侧的$project含常量computed被保留为PROJECTION_DEFAULT与 join 节点正确串联用例 34/35 展示在 join 键上做重命名$project: {m: $a}/ 子管道{n: $a}后谓词变为[m n]或[n m]取决于isLeftChildINLJ 依然可用索引keyPattern: {a: 1}通过表达式追踪还原到a字段用例 36/37 进一步在管道尾部增加$match: {$expr: {$eq: [$m, $x.n]}}将外层$match转化为 join 谓词[m $ n]即尾部 $expr $match 被提升pull-up为连接条件。这组用例证明即便用户强制指定连接方法join 优化与投影下推、表达式重写rename/const依然协同工作并且结果与无 join 优化的基准完全一致例如用例 32 的 9 条结果、用例 36 的 7 条结果。十、37 个用例速览表basic_joins.md全文共 6773 行、37 个用例对应basic_joins_md.js中 37 个section按主题归纳如下#主题关键验证点1sub-pipeline 多 $match谓词合并下推、HJ/NLJ 方法差异2双 join多 join 计划树的 left/right-deep 与 zig-zag 形态3双 join $sortByCountGROUP 之上的 join、投影裁剪4–5双 join 未相关 $match±前缀子管道过滤下推6–7双 join $match 前缀±suffix基表过滤下推8引用前序 lookup 字段依赖型 join 键[x.c c]93 join 跨 lookup 引用依赖约束下的 join ordering、INLJ 索引化103 join 嵌套路径as: w.y深层嵌入11基表 $project投影下推12–13$project 合成字段/重命名PROJECTION_DEFAULT、派生 join 键14join 图环多谓词合并节点15$expr 谓词$谓词、索引化16无谓词 lookup 连通图隐式 join 图17歧义字段投影投影保留 join 键18–25as 字段 $match 吸收谓词吸收/前缀反例/相关子管道26–31$project 系列变体基表/子管道投影下推32–37Hinted INLJ $project/rename$_internalJoinHint、FETCHINDEX_PROBE、$match 提升十一、如何复现与进一步阅读若想在本地验证这些执行计划需要先按 docs/building.md 构建 mongod再通过 resmoke 运行python buildscripts/resmoke.py run --suitesquery_golden jstests/query_golden/join_opt/basic_joins_md.jsquery_goldensuite 配置位于 jstests/suites。生成的输出会与jstests/query_golden/expected_output/internalEnableJoinOptimization/basic_joins.md逐字节对比如果本地 SBE 行为发生变化prettyPrintWinningPlan见 pretty_plan.js负责输出格式一致的 explain 计划树。手动探索时可直接用 mongosh 复现管道并通过db.coll.explain().aggregate(...)观察usedJoinOptimization标志与 winningPlan再配合setParameter切换internalJoinReorderMode、internalJoinPlanTreeShape、internalRandomJoinOrderSeed观察计划变化。需要注意以上参数均为内部实验性参数正式生产环境中应以官方文档为准。相关源码与测试文件索引测试驱动jstests/query_golden/join_opt/basic_joins_md.js基准输出本文主体jstests/query_golden/expected_output/internalEnableJoinOptimization/basic_joins.md测试辅助函数jstests/query_golden/libs/join_opt.js、jstests/libs/query/join_utils.js、jstests/query_golden/libs/pretty_plan.js内部参数定义src/mongo/db/query/query_knob_descriptors_optimization.hexplain 中usedJoinOptimization标志输出src/mongo/db/query/plan_explainer_impl.cpp结语basic_joins.md虽然是一份期望输出文档但它实际上浓缩了 MongoDB join 优化特性的全部核心语义$lookup$unwind到底层 Join 节点的重写、join 顺序与形状的枚举策略、连接方法的自动选择HJ/NLJ/INLJ、谓词吸收与投影下推、跨 lookup 依赖与隐式 join 图、以及 hint 强制机制。理解这份文档等于同时理解了 MongoDB 聚合引擎中关系化优化relational optimization从计划生成到代价选择再到 explain 可视化的完整链路对排查$lookup性能问题、理解 explain 输出、以及为 MongoDB 贡献 join 优化相关代码都具有直接的参考价值。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表