MongoDB 查询系统内部机制:从 CanonicalQuery 到 QuerySolution 的解析、优化与执行架构
2026/9/14 14:04:41 网站建设 项目流程

MongoDB 查询系统内部机制:从 CanonicalQuery 到 QuerySolution 的解析、优化与执行架构

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

本文基于 MongoDB 源码仓库中的 查询系统内部文档 及其关联的 QO 架构指南、逻辑模型文档,系统梳理 MongoDB 查询系统(Query System)的职责边界、从命令到执行计划的完整数据流、核心数据模型(CanonicalQueryMatchExpressionQuerySolution)与执行器架构(PlanExecutor三大子类),并深入 Query Shape、Query Stats 等附加特性。读完后,你可以掌握一条查询从客户端命令到存储引擎的完整生命周期,并能在源码中快速定位查询解析、规划与执行的关键实现。

查询系统的职责边界

查询系统负责三件事:解释用户请求找到满足请求的最优方式计算最终结果(引自 src/mongo/db/query/README.md)。它主要通过findaggregate两条命令对外暴露,同时被大量关联命令复用:

  • 读命令countdistinctmapReduce(后者自 v5.0 起已废弃,推荐用 aggregate 替代——这一点在 QO 架构图 中也有标注);
  • 写命令updatedeletefindAndModify中的 filter 部分同样需要走查询系统解析与规划。

这一"多命令共享"的设计在源码目录结构中可以直接印证:src/mongo/db/query/ 下同时存在find_command.idlcount_command.idldistinct_command.idlgetmore_command.idl等命令定义文件,以及面向写路径的shard_filterer_factory_impl.*(为分片写命令构造分片过滤条件)等文件。

查询系统内部按团队分工可进一步拆为QO(Query Optimization,查询优化)QE(Query Execution,查询执行)两大块:QO 系统的输出是一个获胜的QuerySolution,它正是 QE 系统的输入(原文档 Glossary 中的原话)。

整体架构:从命令到执行计划

README_QO.md 给出了一张高层数据流图,完整呈现了各组件的衔接关系。以下为其原始 mermaid 流程图(引自仓库文档):

从这张图可以读出两条主路径:

  1. find / count / distinct 路径:命令解析为CanonicalQuery;其 filter 部分转化为MatchExpression,经optimizeMatchExpression()启发式重写后进入Plan Enumerator枚举出候选QuerySolution,再由 Classic Multiplanner、面向 SBE 的 Multiplanner 或基于成本的 Cost Based Ranker(辅以 Cardinality Estimation 基数估计与 Cost Model 代价模型)选出获胜计划。
  2. aggregate 路径:命令先解析为PipelineDocumentSource列表),由Pipeline::optimize()判断"能否下推(pushdown)到 find"——能下推则转化为CanonicalQuery走上面的 find 路径,否则交给DocumentSourceExecution,最终两条路径都汇聚到PlanExecutor,再访问 Storage API。

QO 团队维护的核心组件

QO 架构指南 按职责列出了查询优化系统的组件清单,每个组件在仓库中都有对应的文档或源码目录:

组件仓库位置说明
解析(Parsing)commands/query_cmd/README.md、STABLE_API_README.md命令级解析入口,含稳定 API 约束
逻辑模型(Logical Models)README_logical_models.mdCanonicalQuery及其子模型,见下节
MatchExpression 启发式重写matcher/README.mdfilter AST 上的重写规则
Pipeline 启发式重写pipeline/README.md聚合阶段序列上的重写
视图(Views)views/README.md视图展开为聚合管道
计划枚举(Plan Enumeration)query/plan_enumerator/README.md枚举候选 QuerySolution
Classic 运行时规划exec/runtime_planners/classic_runtime_planner/README.mdClassic 引擎的运行时规划器
ExplainREADME_explain.md计划解释与诊断输出
计划缓存(Plan Cache)query/plan_cache/README.md复用重复查询的历史计划
集群规划(Cluster Planning)位于src/mongo/db/s/query/分片规划目录mongos 侧的跨分片规划

此外,该指南还列出了查询测试基础设施:Golden 数据测试框架(docs/golden_data_test_framework.md)、QueryTester 测试库,以及用于模糊测试与负载基准的 fuzzer 和性能测试工具(仓库中可见 query_bm_fixture.h 等基准测试支撑代码)。

核心逻辑模型:CanonicalQuery 及其组件

"逻辑模型"(logical model)是对"数据如何组织、组件如何关联"的一种表示。一个查询的主要逻辑模型就是CanonicalQuery,它自身又是一个容器,把四个组件分别委托给四个子模型(见 README_logical_models.md):

查询组件逻辑模型
filterMatchExpression
projectionProjection
sortSortPattern
distinctCanonicalDistinct

查询规划的第一步以CanonicalQuery为输入;逻辑模型的目标是通过去糖化(desugaring)、规范化(normalization)及其他重写,把所有组件简化到最基础的形式。

CanonicalQuery:解析后的标准查询

从 canonical_query.h 可以看到其实际结构:CanonicalQuery构造函数接收一个CanonicalQueryParams,其中包含表达式上下文(ExpressionContext)、解析后的 find 请求(ParsedFindCommandParsedFindCommandParams二选一)、DocumentSource管道,以及isCountLikeisSearchQueryaggWithNonEmptyPipeline等上下文标志。同一头文件还定义了两类查询形状编码:

  • QueryShapeString:用于计划缓存的形状编码,可对常量做移除或以参数标记替换;
  • PlanCacheCommandKey:与indexFiltersplanCacheClear命令配套的第二种形状编码,用于查找应作用于该查询的索引过滤及应被清除的计划缓存条目。

需要特别澄清 逻辑模型文档 中的一个概念:ParsedFindCommand保存的是 filter、projection、sort 的原始形式,而CanonicalQuery并不创建这些数据,而是把它们的原始形式简化后存储。因此以下三条查询最终得到同一个CanonicalQuery,因为单子节点的$and/$or会在规范化时被消解:

db.c.find({a: 1}) db.c.find({$and: [{a: 4}]}) db.c.find({$or: [{a: 4}]})

若解析后无法生成CanonicalQuery(例如聚合管道超出了可转化为 find 的范畴),查询会跳过优化直接进入查询执行层。

MatchExpression:filter 的抽象语法树

CanonicalQuery持有的 filter 是一棵名为MatchExpression的抽象语法树(AST)。MatchExpression是所有节点的抽象基类,所有可能的节点类型由MatchType枚举穷举,每种类型对应一个子类,例如:

  • EqualityMatchExpression对应MatchType::EQ$eq算子,是 0 子节点的LeafMatchExpression
  • AndMatchExpression对应MatchType::AND$and算子,拥有 N 个子节点(N 为$and的合取项数)。

以这条查询为例:

db.c.find({ $or: [ {a: 1}, {a: 2}, {$and: [{b: {$gte: 0}}, {c: {$lt: 5}}]} ] })

解析阶段(MatchExpressionParser::parse(),实现在 src/mongo/db/matcher/ 目录)把 filter 拆成独立的BSONElement逐个解析,重建为 AST:

OrMatchExpression ├── EqualityMatchExpression a == 1 ├── EqualityMatchExpression a == 2 └── AndMatchExpression ├── GTEMatchExpression b >= 0 └── LTMatchExpression c < 5

CanonicalQuery随后调用MatchExpression::normalize()启动简化。规范化的目标是把 AST 化到最简形式,这样做有两个直接收益:

  1. 由此生成的QuerySolution尽可能简单;
  2. 计划缓存能把逻辑等价的查询识别为等价,从而在初始查询写法不同的情况下也能复用缓存计划。

上例中,两个同字段的$eq析取项可应用$or$in重写规则,规范化后 AST 变为:

OrMatchExpression ├── InMatchExpression a ∈ [1, 2] └── AndMatchExpression ├── GTEMatchExpression b >= 0 └── LTMatchExpression c < 5

这一简化后的 AST 可能源自无穷多条写法更复杂的查询——这正是启发式重写(详见 matcher/README.md)的典型价值。

Projection、SortPattern 与 CanonicalDistinct

Projection同样是 AST。原始BSONObjparseAndAnalyze()转为投影树后,optimizeProjection()使用可变更访问者(ProjectionASTMutableVisitor)递归遍历preVisit/inVisit/postVisit各节点,可原地简化。几个要点:

  • MQL 投影必须是包含型(字段标1)或排除型(字段标0)之一,不能混用;唯一例外是_id默认包含;
  • 嵌套字段、$$elemMatch$slice以及表达式投影(如{$add: ["$a.c", 1]})会展开为更复杂的递归子树;
  • 表达式投影是包含/排除之外的第三类"增补型"投影,投影优化还会利用布尔恒等化简解除虚假依赖(如{x: {$and: [false, "$b"]}}xb的依赖会被释放)。

SortPatternSortPatternPart的向量,每个 part 记录一个字段路径与排序方向,例如:

db.c.find({}).sort({"a": 1, "b.c": -1}) → SortPattern: [ {fieldPath="a", isAscending=true}, {fieldPath="b.c", isAscending=false}, ]

例外是$meta排序(如textScore),它忽略字段名而按元数据作为隐式顶层字段排序。

CanonicalDistinctdistinct()查询或聚合管道命中DISTINCT_SCAN时使用,它只是 distinct 键的容器——filter 仍归CanonicalQuery持有。例如db.c.distinct("x", {x: {$gt: 0}})中,distinct 键"x"CanonicalDistinct保存,而x > 0MatchExpressionCanonicalQuery保存(对应 canonical_distinct.h 等实现)。

去糖化(Desugaring)

MQL 中存在大量"语法糖"。例如:

隐式(语法糖)显式形式
{field: "value"}{field: {$eq: "value"}}
{a: {$gte: 0}, b: {$lt: 5}}{$and: [{a: {$gte: 0}}, {b: {$lt: 5}}]}

去糖化把简写转换回显式形式,让后续优化只需处理单一代码路径(例如统一处理显式$eq,不必再单独处理隐式$eq)。

执行端:PlanExecutor 与 QuerySolution

QuerySolution:一棵执行计划树

QuerySolutionQuerySolutionNode构成的树,表示一个查询的一种可能执行计划。多种操作节点继承自QuerySolutionNode,例如CollectionScanNodeFetchNodeIndexScanNodeOrNode等。通常意义上,一个获胜的QuerySolution是 QO 系统的输出、QE 系统的输入。

PlanExecutor:计划执行器抽象

PlanExecutor是"把一棵QuerySolution的阶段树摇起来执行"的抽象类型,定义于 plan_executor.h。它有三个主要子类(原文档 Glossary 原文):

  1. PlanExecutorImpl—— 执行 find 阶段,实现位于 plan_executor_impl.h;
  2. PlanExecutorPipeline—— 执行聚合(aggregation)阶段,其实现归属于 db/pipeline 模块;
  3. PlanExecutorSBE—— 执行 SBE 计划,实现位于 plan_executor_sbe.h。

从源码结构看,src/mongo/db/query/ 目录中还存在plan_executor_factory.*(执行器工厂)、plan_explainer.*系列(与plan_explainer_impl/express/sbe一一对应的计划解释器)以及engine_selection.*get_executor_deferred_engine_choice*等文件,印证了"运行时在 Classic 与 SBE 引擎之间做延迟选择"的机制;plan_ranker.*plan_ranking/sbe_plan_ranker.h等则对应架构图中"Cost Based Ranker 与基数估计、代价模型协同选优"的环节。

Pipeline、DocumentSource 与 Pushdown

  • PipelineDocumentSource的列表,承载查询的一部分优化工作;
  • DocumentSource:表示聚合管道中的一个阶段,与用户定义管道中的阶段不必然一一对应(重写、下推都会改变阶段数量与形态);
  • Pushdown:把管道中的某个 aggregate 阶段转化为 find 阶段——这正是架构图里"Can pushdown to find?"分支的来源,也是query_planner_pipeline_pushdown_test.cpp等测试所验证的行为。

LiteParsedPipeline 与 ExpressionContext

  • LiteParsedPipeline:通过"半解析"(semi-parse)构建的极简管道模型,只解析到足以把各阶段拆分的程度——它既没有校验输入良构性,也没有解析表达式或阶段细节参数。它服务于"在决定是否构建完整模型之前先检查请求"的场景;
  • ExpressionContext:保存整个查询生命周期中可能有用、但与其他操作无关的状态,包括 collation、时区数据库、若干随机布尔与运行状态等。从 canonical_query.h 可见它以ExpressionContext指针的形式作为CanonicalQueryParams的一部分贯穿规划过程。

附加查询特性

主 README 列出了一组"附加查询特性",每项在仓库中都有独立的文档与实现目录:

Query Shape(查询形状)

query_shape/README.md 定义了"查询形状":命令的变形版本,其中字面值被替换为规范 BSON 类型占位符。同一命令的不同实例,一旦抽象掉字面值后相同,就具有相同的形状:

db.example.findOne({x: 24}); // 同形状 db.example.findOne({x: 53}); // 同形状

而不同字面同形状,不同 BSON类型则不同形状({x: 53}{x: "string"}形状不同)。该概念覆盖大部分 CRUD 命令:读命令(distinct、count、aggregate)与写命令(update、delete、insert)都有各自的形状。update/delete 像读命令一样对 filter 形状化,并额外纳入写专属组件(如 delete 的multi标志、update 的修改语句与multi/upsert标志);insert 没有谓词,其形状即集合名加恒定为?array<?object>documents字段。

形状由一组"形状组件"类决定每个命令哪些字段参与定形,类层次为CmdSpecificShapeComponentsFindCmdShapeComponentsAggCmdShapeComponentsCountCmdShapeComponentsDistinctCmdShapeComponentsDeleteCmdShapeComponentsInsertCmdShapeComponentsUpdateCmdShapeComponentsLetShapeComponent等,一一对应 query_shape/ 目录下的find_cmd_shape.hagg_cmd_shape.h等文件。

形状化有三种序列化选项(SerializationOptions):

  • kUnchanged:字面值原样保留,{x: 5, y: "hello"}{x: 5, y: "hello"}
  • kToDebugTypeString:输出人类可读类型串,→{x: "?number", y: "?string"}
  • kToRepresentativeParseableValue:每种类型序列化为一个固定代表值且必须可解析,→{x: 1, y: "?"};为保证可解析性做了特殊处理,例如正则{x: {$regex: "^p.*"}}会序列化为{x: {$regex: "\\?"}},因为"?"本身不是合法正则。

计算形状哈希时采用第三种选项——同类型的所有字面量收敛为同一值,从而产生同一哈希,实现"shapify"分组。

Query Stats(查询统计)

query_stats/README.md 描述了运行时查询统计基础设施。核心是QueryStatsStore:一个分片哈希表,以"查询统计键"(Query Stats Key)的哈希为键,聚合每次成功执行后的指标。查询统计键比查询形状粒度更细——例如db.example.find({x: 55}).batchSize(2)batchSize(3)形状相同,但batchSize作为额外维度会使其落入不同的统计条目。

关键配置项(服务端参数):

参数含义与默认值
internalQueryStatsCacheSize统计存储上限,可写 "4MB" 或 "1%" 形式,默认机器总内存的 1%;存储为分片 LRU 缓存,超限按 LRU 驱逐
internalQueryStatsRateLimit基于窗口的限流:每秒最大记录数,整数;默认 0(禁用收集),-1 表示不限流
internalQueryStatsSampleRate基于采样的限流:0.0–1.0 的小数,表示被记录查询的比例;默认 0;两者同时非零时采样策略优先
internalQueryStatsWriteCmdSampleRate整数 0/1,控制写命令是否参与统计,仅在上述限流已启用时生效
logComponentVerbosity.queryStats控制日志行为:1 级记录带hmac-sha-256$queryStats调用(密钥脱敏),3 级记录其全部结果

统计的采集流程:规划阶段调用registerRequest按形状加各维度生成键并挂在opDebug上(支持getMore的命令同时挂在游标上以便跨批次累计);执行完成后writeQueryStats更新或新建条目。写命令不走游标,且update/delete语句逐个注册(每条语句有独立q谓词与独立计划),insert则按命令整体注册。

统计通过$queryStats聚合阶段从 admin 库读取(必须位于管道首位),可选transformIdentifiers(仅支持hmac-sha-256算法,需hmacKey)对字段/集合/库名做单向令牌化,实现"能识别两条查询用了同一标识符,但永不泄露标识符本身"。输出文档包含keykeyHashqueryShapeHash(可与持久化查询设置交叉引用)、asOfmetrics(总执行时长、CPU 时间、keysExamined/docsExamined/bytesRead、规划器指标fromPlanCache/planningTimeMicros、写指标nMatched/nDeleted等,完整字段表见 query_stats/README.md)。权限上由queryStatsReadqueryStatsReadTransformed两个动作控制。serverStatus.metrics.queryStats还会暴露numEvictednumRateLimitedRequestsqueryStatsStoreSizeEstimateBytes等运行计数。

其他特性

  • Change Streams:查询统计对 change stream 有特殊行为——每次getMore被当作独立查询记录,而非累计在游标上一次性记录;
  • Field-Level Encryptionfle/目录位于 query 目录下,承载字段级加密与可查询加密(Queryable Encryption)相关查询逻辑;
  • Geo:地理查询支持;
  • Search:search/README.md 是搜索特性的入口页,其中 search 是 mongot 管道阶段($search$vectorSearch$searchMeta)的统称,涵盖技术概览、基于 mongot-mock 与真实 mongot 二进制的三类测试方式,以及 Hybrid Search($rankFusion基于 RRF、$scoreFusion基于自定义得分组合)与scoreDetails四阶段构建机制;
  • Timeseries:timeseries/README.md 讲述时序集合的查询翻译与优化:9.0 前的"viewful"时序集合中用户命名空间是一个视图,find/count/distinct/aggregate会被改写为对底层system.buckets.<namespace>集合、并在管道首部插入$_internalUnpackBucket阶段的聚合请求;9.0 起为单一命名空间的 "viewless" 时序集合,不再有桶集合与视图解析步骤。

术语表(Glossary)

以下术语表完整继承自 src/mongo/db/query/README.md 的 Glossary 章节(中文为补充解释):

术语定义
Aggregation(聚合)运行 aggregate 阶段的子系统
BSON类 JSON 文档的二进制序列化格式,MongoDB 核心数据表示所用
CanonicalQuery查询的 BSON 标准形式;承载解析后的 query、projection、sort 三部分,其中 filter 部分被解析为MatchExpression
DocumentSource表示聚合管道中的一个阶段;与用户定义管道中的阶段不必然一一对应
ExpressionContext保存整个查询生命周期内可能有用、与其他操作无关的状态对象:collation、时区数据库、各种随机布尔与状态等
Find运行 find 阶段与已下推(pushed-down)聚合阶段的子系统
IDL接口定义语言,用 YAML 文件生成 C++ 代码(如命令定义*.idl文件)
LiteParsedPipeline通过半解析构建的极简聚合管道模型,只拆分出所涉阶段;既未验证输入良构,也未解析表达式或详细参数;用于在构建完整模型前检查请求
MatchExpression查询 filter 部分解析得到的抽象语法树(AST)
MQLMongoDB Query Language
Plan Cache(计划缓存)存储历史生成的查询计划,使重复查询免于重新生成与评分候选计划
PlanExecutor通过驱动一棵QuerySolution的阶段树执行计划来执行计划的抽象类型;三个主要子类:PlanExecutorImpl(find 阶段)、PlanExecutorPipeline(聚合阶段)、PlanExecutorSBE(SBE 计划)
PipelineDocumentSource列表,处理优化的一部分工作
Pushdown(下推)把管道中的 aggregate 阶段转化为 find 阶段
QuerySolutionQuerySolutionNode的树结构,表示一个查询的一种可能执行计划;CollectionScanNodeFetchNodeIndexScanNodeOrNode等操作节点继承自QuerySolutionNode;一个获胜的QuerySolution通常就是 QO 系统的输出与 QE 系统的输入

延伸阅读:关键文档与源码索引

内容路径
查询系统总览(本文主体文档)src/mongo/db/query/README.md
QO 架构指南(含高层数据流图)src/mongo/db/query/README_QO.md
逻辑模型详解src/mongo/db/query/README_logical_models.md
Explain 机制src/mongo/db/query/README_explain.md
查询功能特性开关src/mongo/db/query/README_query_feature_flags.md
Query Shape vs Query Stats 辨析src/mongo/db/query/README_query_shape_disambiguation.md
CanonicalQuery定义src/mongo/db/query/canonical_query.h
PlanExecutor抽象src/mongo/db/query/plan_executor.h
计划缓存src/mongo/db/query/plan_cache/README.md
计划枚举src/mongo/db/query/plan_enumerator/README.md
QueryTester 测试库src/mongo/db/query/query_tester/README.md
Golden 数据测试框架docs/golden_data_test_framework.md

以上路径均可在当前仓库中直接查阅,配合本文的架构脉络,可以完整走读一条查询从命令解析、逻辑模型构建、计划枚举与评分,到PlanExecutor执行与结果返回的全链路实现。

【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询