ARTICLE DETAIL

资讯详情

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

MongoDB 聚合 Pipeline 重写与优化:从启发式改写机制到规则注册实践

MongoDB 聚合 Pipeline 重写与优化:从启发式改写机制到规则注册实践 MongoDB 聚合 Pipeline 重写与优化从启发式改写机制到规则注册实践【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文以 Pipeline 优化模块文档 为主体深入讲解 MongoDB 聚合管道在被解析为Pipeline后如何经历两阶段启发式重写跨阶段优化阶段交换、合并、插入与阶段内优化常量折叠、无操作阶段消除。读完本文你将掌握optimizePipeline()的完整调用链、依赖追踪的五大依赖类别、如何基于REGISTER_RULES宏注册新的重写规则以及如何用disablePipelineOptimization故障点验证改写语义正确性。一、总览Pipeline 重写的两步走用户在aggregate命令中提交的管道解析为Pipeline之后会经历启发式重写heuristic rewrites把整条管道以及管道内的各个阶段改写为更高效的等价形式。优化入口是 optimizePipeline() 函数它包含两个主要步骤跨阶段优化Inter-stage Optimization优化整个Pipeline对象。它在内部表现为DocumentSource的容器优化过程通过合并combining、交换swapping、删除dropping和插入inserting阶段来修改这个容器。阶段内优化Stage-specific Optimization逐个优化每个阶段即每个DocumentSource。从源码实现看optimize.cpp 中的optimizePipeline()在确认管道未被冻结isFrozen()且未被故障点禁用后按顺序调用两轮规则引擎applyRuleBasedRewrites(rbr::PipelineRewriteContext(pipeline), Tags::Reordering); applyRuleBasedRewrites(rbr::PipelineRewriteContext(pipeline), Tags::InPlace);两轮分别只运行带Reordering标签和InPlace标签的规则且每次重写引擎都受internalQueryMaxPipelineRewrites查询旋钮的改写次数上限保护见 optimize.cpp#L19-L30。禁用优化以便验证语义如果你新增了某个重写规则想验证其语义正确性可以将开启优化的结果与未优化形态的执行结果做对比。仓库提供了两个手段打开disablePipelineOptimization故障点阻止单个DocumentSource被优化。在 mongo shell 中执行db.adminCommand({configureFailPoint: disablePipelineOptimization, mode: alwaysOn})对应源码即 optimize.cpp#L41-L43 中对MONGO_unlikely(disablePipelineOptimization.shouldFail())的短路检查。在管道中每个阶段之前插入{$_internalInhibitOptimation: {}}阶段确保各阶段不参与整条Pipeline级的优化如阶段下推或交换。二、跨阶段优化交换、合并与插入阶段跨阶段优化的入口是 optimizeContainer()它调用基于规则的重写引擎执行所有可能组合或重排相邻阶段、或以其他形式修改管道结构的规则。目前除$match、$sample、$project、$redact的下推外全部跨阶段重写都实现在各DocumentSource的公开optimizeAt()方法中并注册为无条件规则unconditional rules。例如 qo_rules_to_move.cpp 中可以看到为DocumentSourceSkip、DocumentSourceLimit、DocumentSourceGroup、DocumentSourceUnionWith、DocumentSourceUnwind、DocumentSourceSort等逐一注册OPTIMIZE_AT_RULE与OPTIMIZE_IN_PLACE_RULE。跨阶段优化大致分为三类2.1 交换阶段Swapping stages典型例子是$match 下推。一般来说希望在文档进入计算更重的阶段之前先过滤以最小化工作量。如果用户把$match写在$sort之后优化器会尽可能将其下推以最小化需要排序的文档数量。例如用户写出{ $sort: { age : -1 } }, { $match: { status: A } }优化器会将其改写为{ $match: { status: A } }, { $sort: { age : -1 } }这一能力的落地是 match_rules.cpp 中注册的MATCH_PUSHDOWN规则match_rules.cpp#L490-L499前置条件为matchCanSwapWithPrecedingStage变换函数为pushMatchBeforePrecedingStage优先级为kDefaultPushdownPriority100.0所有规则中的最高档标签为Reordering。值得注意的是$match 并非总能整段交换pushdownMatch()会先调用DocumentSourceMatch::splitMatchByModifiedFields()把谓词按“可被前级阶段改名/修改的字段”拆成两部分match_rules.cpp#L340-L368再通过Transforms::partialPushdown()把可下推部分移到前级阶段之前、不可下推部分保留在原地。另外源码中还能看到一些精细的守卫逻辑例如groupMatchSwapVerified()会阻止与$group桶化语义冲突的谓词$exists、$type、部分$expr下推match_rules.cpp#L178-L250以及文本搜索谓词的$match本就要求位于管道首位因而无需下推。2.2 合并阶段Coalescing stages在一系列重排优化之后优化器尽可能把某阶段合并进其前驱阶段。例如当$sort位于$limit之前、且中间没有会改变文档数量的阶段如$unwind或$group时优化器可以把$limit吸收进$sort。给定{ $sort : { age : -1 } }, { $project : { age : 1, status : 1, name : 1 } }, { $limit: 5 }优化器会改写为{ $sort : { sortKey : { age : -1 }, limit : NumberLong(5) } }, { $project : { age : 1, status : 1, name : 1 } }这样排序阶段在推进过程中只需保留前 N 条结果N 为 limit 值减少了需要驻留内存的文档数量。两个连续的同名阶段也可以合并等价于删除其中一个。例如相邻的两个$limit可以合并为其中较小的一个{ $limit: 100 }, { $limit: 10 }改写为{ $limit: 10 }2.3 插入阶段Inserting stages虽然看似反直觉但向管道中插入阶段有时是有益的它可以约束传递给下游阶段的文档流并利用更优的索引。例如当一个$redact后面紧跟$match时可以把$match中的一部分“上插”到$redact之前。给定{ $redact: { $cond: { if: { $gte: [ $sensitivity, 3 ] }, then: $$PRUNE, else: $$DESCEND } } }, { $match: { status: active, sensitivity: { $lt: 5 } } }优化器可以推断出status字段与$redact阶段无关而sensitivity字段与之相关于是把$match拆分为独立部分与依赖部分——前者推到$redact之前后者保留在之后{ $match: { status: active } }, { $redact: { $cond: { if: { $gte: [ $sensitivity, 3 ] }, then: $$PRUNE, else: $$DESCEND } } }, { $match: sensitivity: { $lt: 5 } }改写后的管道阶段数变多了但执行更优因为它同时1减少了进入资源密集$redact阶段的文档量2让第一个$match可以利用status上的索引。除了交换与合并仓库中还存在“删除冗余阶段”这类重写。例如 sort_rules.cpp 注册的REDUNDANT_SORT_REMOVAL规则当$sort的排序模式已被上游某阶段建立的排序模式所扩展isExtensionOf、且中间各阶段满足preservesOrderAndMetadata约束、不会改名或重算排序键字段时当前$sort被Transforms::eraseCurrent直接删除。前置条件sortIsRedundantGivenPrecedingStages()还会特意排除已吸收$limit的$sort与时序集_timeSorter等带有额外执行语义的形态sort_rules.cpp#L67-L112。2.4 依赖追踪与分析为了判断管道是否合法、阶段能否互相下推、或某个阶段的部分谓词能否裁剪优化器依赖依赖追踪与分析dependency tracking and analysis识别每个阶段执行所依赖的字段或变量。若某阶段依赖更早的阶段它在整个重写过程中必须保持在该阶段之后反之若某阶段与前一阶段独立且下推有利则可以安全地下推。主要依赖类别有文档字段依赖Document field dependencies阶段所需的特定文档字段。例如{$project: {name: 1}}依赖name字段。计算字段Computed fields由$addFields、$group等阶段生成的新字段可能成为后续阶段的依赖。例如{$addFields: {total: {$sum: [price, $tax]}}}计算出total可被后续阶段引用。改名Renames改变字段名的映射关系后续阶段必须感知这些变换才能解析正确字段。例如{$project: {newField: $oldField}}把oldField改名为newField。变量引用Variable references对作用域变量的依赖如用户自定义变量或系统变量$$CURRENT、$$ROOT。例如{ $project: { adjustedValue: { $let: { vars: { discount: 0.1 }, in: { $multiply: [$price, { $subtract: [1, $$discount] }] } } } } }其中$$discount就是在$let内定义并使用的变量引用。元数据Metadata不属于文档核心字段的附加信息常出现在依赖上下文信息的操作中如文本搜索得分、地理邻近度或保留字段。例如{$project: {score: {$meta: textScore}}}依赖的是textScore元数据而非文档字段。依赖追踪与校验的实现细节可参阅 expression_algo.h、semantic_analysis.h 和 dependencies.h。从源码结构看重写上下文还维护了DependencyGraph阶段间顺序依赖图见 rule_based_rewriter.h 中的getDependencyGraph()与 rule_based_rewriter.cpp 中postTransform()的图失效逻辑每次变换之后上下文会保守地把从上一位置起的依赖图标记为失效从而保证后续规则看到的依赖信息是最新的。三、阶段内优化单阶段重写与常量折叠确定阶段最终顺序后系统会再次调用规则引擎但这次使用只运行阶段内优化规则的配置。这些规则目前实现为DocumentSource子类的公开optimize()方法并注册为无条件规则。每条规则要么返回一个语义等价的优化后DocumentSource要么在当前阶段是 no-op 时将其删除。例如 no-op 阶段{$match: {}}会被直接移除。$match阶段中的MatchExpression包含专门的重写逻辑详见 MatchExpression 说明。此外含有ExpressionConstant值的Expression的阶段可能符合常量折叠条件。例如{ $project: { a: { $sum: [ 4, 5, 1 ] } } }其中的常量可以折叠为一个{ $project: { a: { $literal: 10 } } }常量折叠Constant folding对包含常量或可解析为常量的表达式求值并用计算结果替换原表达式。在查询规划期简化表达式从而降低执行期的计算开销。表达式Expression查询中解析为某个值的组件。它是无状态的即返回一个值而不改变用于构建表达式的任何值。例如表达式{$add: [3, $inventory.total]}由$add运算符与两个输入表达式构成常量3和字段路径表达式$inventory.total它返回输入文档在路径inventory.total处取值加 3 的结果。这些优化看起来显然但聚合管道往往由计算机生成应用层通常不会也不应该做这类分析。而且原始查询可能更复杂经过前面的启发式重写后可能发现比最初预期更多的值可以合并折叠。整个优化流程如下四、注册新的重写规则所有管道重写都经由基于规则的重写引擎触发。虽然多数重写目前仍实现在DocumentSource::optimizeAt()与optimize()中并注册为无条件规则即前置条件恒为真但新的重写应当实现并注册为独立的规则。一条规则由名称、前置条件与变换函数、优先级priority以及一组标签tags定义见 Rule 结构体precondition决定是否执行transformtransform应用规则本身返回值指示引擎是否需要重排/重放当前位置priority数值越大优先级越高同位置多条规则命中时按此排序执行tags允许引擎只运行某一子集规则。引擎的核心循环在 RewriteEngine::applyRules()对每个元素先向上下文收集可应用规则然后按优先级尝试根据变换是否改变了当前位置来决定重放Requeue还是前进Advance。4.1 规则注册表与注册宏规则注册表Rule registry 是DocumentSource子类型与可应用规则之间的映射它作为ServiceContext的 decoration 存在rule_based_rewriter.cpp#L73-L84。这意味着规则注册在创建服务上下文时即新 mongod/mongos 进程启动时被调用。每当重写引擎推进到新元素就会调用 PipelineRewriteContext::enqueueRules()其中按当前DocumentSource的类型查表并检查规则关联的 feature flag 是否启用未启用时以kLastLTS作为回退 FCV 判断把适用规则入队。规则可通过REGISTER_RULES宏注册第一个参数是DocumentSource子类随后是逗号分隔的规则列表。以DocumentSourceMatch的注册为例match_rules.cpp#L490-L499REGISTER_RULES(DocumentSourceMatch, OPTIMIZE_AT_RULE(DocumentSourceMatch), OPTIMIZE_IN_PLACE_RULE(DocumentSourceMatch), { .name MATCH_PUSHDOWN, .precondition matchCanSwapWithPrecedingStage, .transform pushMatchBeforePrecedingStage, .priority kDefaultPushdownPriority, .tags PipelineRewriteContext::Tags::Reordering, });宏内部展开为ServiceContext::ConstructorActionRegisterer静态注册器rule_based_rewriter.h#L48-L70OPTIMIZE_AT_RULE(DS)与OPTIMIZE_IN_PLACE_RULE(DS)两个辅助宏分别把DocumentSource的optimizeAt()与optimize()包装成无条件规则。仓库为不同用途的规则约定了默认优先级rule_based_rewriter.h#L96-L103常量值用途kDefaultPushdownPriority100.0尽量早地推入$match等高优先级下推kDefaultHoistPriority50.0条件性地提升计算以促成更多$match下推kDefaultOptimizeAtPriority10.0与相邻阶段交换或吸收kDefaultOptimizeInPlacePriority1.0原地优化阶段内部若需让规则受 feature flag 门控使用REGISTER_RULES_WITH_FEATURE_FLAG宏用法与REGISTER_RULES类似但第二个参数为 feature flag。例如 match_rules.cpp#L513-L521 中PUSH_MATCH_BEFORE_SINGLE_DOC_TRANSFORMATION规则就挂在gFeatureFlagImprovedDepsAnalysis之下。另一种让规则被条件调用的方式是在另一条规则的前置条件或变换函数中调用RewriteContext::addRule()动态入队。需要注意如果引擎被配置为只运行某一组规则动态入队的规则只有在同属该组时才会执行。例如规则PUSH_MATCH_BEFORE_CHANGE_STREAMS就是在canPushMatchBefore()检测到前级阶段是 mongos 内部 change stream 阶段时通过ctx.addRule()入队的match_rules.cpp#L291-L297。4.2 标签Tags当前部分重写依赖一个假设所有跨阶段优化都在任何原地优化之前对整条管道完成。若此假设被破坏重写之间可能互相干扰。因此现有管道重写被划分为两类标签Reordering可能改变当前阶段之外其他阶段的规则如重排、合并、删除阶段InPlace只优化阶段内部、从不触碰相邻阶段的规则另有Testing标签的规则仅在enablePipelineOptimizationAdditionalTestingRules查询旋钮开启时应用见 optimize.cpp#L52-L55。两组成分是分别对管道执行的。编写新规则时的选择原则很简单如果你的规则可能改变当前阶段以外的任何阶段就给它Reordering标签否则给它InPlace标签。五、小结与延伸阅读Pipeline 优化机制可以概括为optimizePipeline()先用Reordering轮驱动跨阶段的结构改写下推、合并、插入、冗余删除再用InPlace轮驱动各阶段内部的等价重写无操作消除、常量折叠等而全部规则通过ServiceContext级别的注册表 feature flag 体系集中管理支持按标签分组、按优先级调度、按条件动态入队。希望继续深入时建议按以下路径阅读优化入口与两轮驱动optimize.cpp管道重写上下文、标签与Transforms原语rule_based_rewriter.h、rule_based_rewriter.cpp通用规则引擎设计query/compiler/rewrites/README.md、rule_based_rewriter.h$match下推与拆分match_rules.cpp冗余$sort删除sort_rules.cpp批量optimizeAt注册qo_rules_to_move.cpp端到端测试pipeline_rewriter_test.cpp各阶段MatchExpression重写细节matcher/README.md【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表