MongoDB 查询系统内部机制:从 CanonicalQuery 到 QuerySolution 的解析、优化与执行架构
【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo
本文基于 MongoDB 源码仓库中的 查询系统内部文档 及其关联的 QO 架构指南、逻辑模型文档,系统梳理 MongoDB 查询系统(Query System)的职责边界、从命令到执行计划的完整数据流、核心数据模型(CanonicalQuery、MatchExpression、QuerySolution)与执行器架构(PlanExecutor三大子类),并深入 Query Shape、Query Stats 等附加特性。读完后,你可以掌握一条查询从客户端命令到存储引擎的完整生命周期,并能在源码中快速定位查询解析、规划与执行的关键实现。
查询系统的职责边界
查询系统负责三件事:解释用户请求、找到满足请求的最优方式、计算最终结果(引自 src/mongo/db/query/README.md)。它主要通过find和aggregate两条命令对外暴露,同时被大量关联命令复用:
- 读命令:
count、distinct、mapReduce(后者自 v5.0 起已废弃,推荐用 aggregate 替代——这一点在 QO 架构图 中也有标注); - 写命令:
update、delete、findAndModify中的 filter 部分同样需要走查询系统解析与规划。
这一"多命令共享"的设计在源码目录结构中可以直接印证:src/mongo/db/query/ 下同时存在find_command.idl、count_command.idl、distinct_command.idl、getmore_command.idl等命令定义文件,以及面向写路径的shard_filterer_factory_impl.*(为分片写命令构造分片过滤条件)等文件。
查询系统内部按团队分工可进一步拆为QO(Query Optimization,查询优化)与QE(Query Execution,查询执行)两大块:QO 系统的输出是一个获胜的QuerySolution,它正是 QE 系统的输入(原文档 Glossary 中的原话)。
整体架构:从命令到执行计划
README_QO.md 给出了一张高层数据流图,完整呈现了各组件的衔接关系。以下为其原始 mermaid 流程图(引自仓库文档):
从这张图可以读出两条主路径:
- find / count / distinct 路径:命令解析为
CanonicalQuery;其 filter 部分转化为MatchExpression,经optimizeMatchExpression()启发式重写后进入Plan Enumerator枚举出候选QuerySolution,再由 Classic Multiplanner、面向 SBE 的 Multiplanner 或基于成本的 Cost Based Ranker(辅以 Cardinality Estimation 基数估计与 Cost Model 代价模型)选出获胜计划。 - aggregate 路径:命令先解析为
Pipeline(DocumentSource列表),由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.md | CanonicalQuery及其子模型,见下节 |
| MatchExpression 启发式重写 | matcher/README.md | filter 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.md | Classic 引擎的运行时规划器 |
| Explain | README_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):
| 查询组件 | 逻辑模型 |
|---|---|
| filter | MatchExpression |
| projection | Projection |
| sort | SortPattern |
| distinct | CanonicalDistinct |
查询规划的第一步以CanonicalQuery为输入;逻辑模型的目标是通过去糖化(desugaring)、规范化(normalization)及其他重写,把所有组件简化到最基础的形式。
CanonicalQuery:解析后的标准查询
从 canonical_query.h 可以看到其实际结构:CanonicalQuery构造函数接收一个CanonicalQueryParams,其中包含表达式上下文(ExpressionContext)、解析后的 find 请求(ParsedFindCommand或ParsedFindCommandParams二选一)、DocumentSource管道,以及isCountLike、isSearchQuery、aggWithNonEmptyPipeline等上下文标志。同一头文件还定义了两类查询形状编码:
QueryShapeString:用于计划缓存的形状编码,可对常量做移除或以参数标记替换;PlanCacheCommandKey:与indexFilters和planCacheClear命令配套的第二种形状编码,用于查找应作用于该查询的索引过滤及应被清除的计划缓存条目。
需要特别澄清 逻辑模型文档 中的一个概念: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 < 5CanonicalQuery随后调用MatchExpression::normalize()启动简化。规范化的目标是把 AST 化到最简形式,这样做有两个直接收益:
- 由此生成的
QuerySolution尽可能简单; - 计划缓存能把逻辑等价的查询识别为等价,从而在初始查询写法不同的情况下也能复用缓存计划。
上例中,两个同字段的$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。原始BSONObj经parseAndAnalyze()转为投影树后,optimizeProjection()使用可变更访问者(ProjectionASTMutableVisitor)递归遍历preVisit/inVisit/postVisit各节点,可原地简化。几个要点:
- MQL 投影必须是包含型(字段标
1)或排除型(字段标0)之一,不能混用;唯一例外是_id默认包含; - 嵌套字段、
$、$elemMatch、$slice以及表达式投影(如{$add: ["$a.c", 1]})会展开为更复杂的递归子树; - 表达式投影是包含/排除之外的第三类"增补型"投影,投影优化还会利用布尔恒等化简解除虚假依赖(如
{x: {$and: [false, "$b"]}}中x对b的依赖会被释放)。
SortPattern是SortPatternPart的向量,每个 part 记录一个字段路径与排序方向,例如:
db.c.find({}).sort({"a": 1, "b.c": -1}) → SortPattern: [ {fieldPath="a", isAscending=true}, {fieldPath="b.c", isAscending=false}, ]例外是$meta排序(如textScore),它忽略字段名而按元数据作为隐式顶层字段排序。
CanonicalDistinct在distinct()查询或聚合管道命中DISTINCT_SCAN时使用,它只是 distinct 键的容器——filter 仍归CanonicalQuery持有。例如db.c.distinct("x", {x: {$gt: 0}})中,distinct 键"x"由CanonicalDistinct保存,而x > 0的MatchExpression由CanonicalQuery保存(对应 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:一棵执行计划树
QuerySolution是QuerySolutionNode构成的树,表示一个查询的一种可能执行计划。多种操作节点继承自QuerySolutionNode,例如CollectionScanNode、FetchNode、IndexScanNode、OrNode等。通常意义上,一个获胜的QuerySolution是 QO 系统的输出、QE 系统的输入。
PlanExecutor:计划执行器抽象
PlanExecutor是"把一棵QuerySolution的阶段树摇起来执行"的抽象类型,定义于 plan_executor.h。它有三个主要子类(原文档 Glossary 原文):
PlanExecutorImpl—— 执行 find 阶段,实现位于 plan_executor_impl.h;PlanExecutorPipeline—— 执行聚合(aggregation)阶段,其实现归属于 db/pipeline 模块;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
- Pipeline:
DocumentSource的列表,承载查询的一部分优化工作; - 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字段。
形状由一组"形状组件"类决定每个命令哪些字段参与定形,类层次为CmdSpecificShapeComponents→FindCmdShapeComponents、AggCmdShapeComponents、CountCmdShapeComponents、DistinctCmdShapeComponents、DeleteCmdShapeComponents、InsertCmdShapeComponents、UpdateCmdShapeComponents、LetShapeComponent等,一一对应 query_shape/ 目录下的find_cmd_shape.h、agg_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)对字段/集合/库名做单向令牌化,实现"能识别两条查询用了同一标识符,但永不泄露标识符本身"。输出文档包含key、keyHash、queryShapeHash(可与持久化查询设置交叉引用)、asOf及metrics(总执行时长、CPU 时间、keysExamined/docsExamined/bytesRead、规划器指标fromPlanCache/planningTimeMicros、写指标nMatched/nDeleted等,完整字段表见 query_stats/README.md)。权限上由queryStatsRead与queryStatsReadTransformed两个动作控制。serverStatus.metrics.queryStats还会暴露numEvicted、numRateLimitedRequests、queryStatsStoreSizeEstimateBytes等运行计数。
其他特性
- Change Streams:查询统计对 change stream 有特殊行为——每次
getMore被当作独立查询记录,而非累计在游标上一次性记录; - Field-Level Encryption:
fle/目录位于 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) |
| MQL | MongoDB Query Language |
| Plan Cache(计划缓存) | 存储历史生成的查询计划,使重复查询免于重新生成与评分候选计划 |
PlanExecutor | 通过驱动一棵QuerySolution的阶段树执行计划来执行计划的抽象类型;三个主要子类:PlanExecutorImpl(find 阶段)、PlanExecutorPipeline(聚合阶段)、PlanExecutorSBE(SBE 计划) |
| Pipeline | DocumentSource列表,处理优化的一部分工作 |
| Pushdown(下推) | 把管道中的 aggregate 阶段转化为 find 阶段 |
QuerySolution | QuerySolutionNode的树结构,表示一个查询的一种可能执行计划;CollectionScanNode、FetchNode、IndexScanNode、OrNode等操作节点继承自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),仅供参考