第34章:Mongo查询执行引擎源码——一次 find 是怎样跑完的
1. 项目背景业务场景DBA 在慢查询日志中发现一个find({status:在售}).sort({createdAt:-1}).limit(20)的执行计划中有 SORT 阶段但明明有一个{status:1, createdAt:-1}的复合索引。为什么优化器没用它开发用 explain(“allPlansExecution”) 看到这个索引出现在 rejectedPlans 中——原因是优化器的竞速阶段中它返回第一批结果的速度比另一个候选计划慢了几毫秒就被淘汰了。但开发不服气——“明明用了它就没有 SORT 了凭什么淘汰” 要回答这个问题需要深入查询执行引擎的源码——看 Plan Enumerator 是怎么枚举候选计划的、Stage 树是怎么构建的、SBE 和 Classic Engine 在这个查询上的执行路径有何差异。痛点只看 explain 的 winningPlan 而不理解内部机制就像医生只看 X 光片而不懂解剖学。优化器选错索引时不知道是统计信息问题还是竞速算法问题对 SBE 和 Classic 的执行差异不理解升级版本后发现索引行为变化却无法解释。2. 项目设计小胖指着 explain 输出大师优化器在我的查询上拒绝了一个完美的索引选了个有 SORT 阶段的我要给它提 bug大师先别急看看 rejectedPlans 里那套索引的竞速数据——它的 totalKeysExamined 是多少小胖嗯……rejected 的那个扫描了 12000 个键winning 的扫描了 2000 个键。但 winning 有 SORT大师这就是 Plan Enumerator 的竞速机制——每个候选计划在索引上跑一小段哪个最先返回 101 个文档的批次默认 batchSize哪个就赢。你的{status:1, createdAt:-1}索引虽然最终能消除 SORT但因为这个索引要按 createdAt 降序扫描——降序扫描在 B-Tree 上的成本比升序略高——加上 status 的选择性不好status 只有几个值索引扫描的范围很大。另一个索引虽然多了一个 SORT 阶段但它的等值过滤更高效返回首批结果更快——所以竞速赢了。技术映射Plan Enumerator 生成所有可能的候选计划不同索引组合 排序策略每个候选跑一小段trial period返回 first batch 最快的计划获胜。这个竞速算法是近似最优而非全局最优。小胖那 Stage 树是什么explain 里那些 COLLSCAN、IXSCAN、FETCH、SORT、LIMIT 是怎么串起来的大师Stage 树是查询执行计划的物理表示——每个 Stage 是一个独立的执行单元从子 Stage 获取数据处理后传给父 Stage。一棵 Stage 树从叶到根是这样LIMIT (限制返回 20 条) └── SORT (按 createdAt 排序) └── FETCH (从集合中读取完整文档) └── IXSCAN (在索引中查找符合条件的 _id)每个 Stage 都有两个核心方法work()做一步工作和getNext()获取下一个结果。引擎通过 Yield让出锁机制在 Stage 之间切换实现非阻塞执行。技术映射Stage 树 查询的物理执行计划。数据从叶子 Stage 流向根 Stage每个 Stage 可以提前终止如 LIMIT 收够了就停。小白那 SBESlot-Based Execution和 Classic Engine 的区别是什么为什么 MongoDB 5.0 默认 SBE大师Classic Engine 是基于 Stage 树的——每个 Stage 是独立对象数据通过getNext()方法在 Stage 之间拉pull-based。SBE 则用基于槽位的表达式求值——把查询计划编译成一系列低级的 VM 指令在寄存器slot上直接运算避免了 Stage 之间的函数调用开销。可以这样类比——Classic Engine 是解释型语言每个 Stage 独立执行SBE 是 JIT 编译查询编译成 VM 指令后高效执行。在包含复杂表达式如$addFields、$project中做了计算的查询中SBE 比 Classic 快 20%-50%。技术映射SBE 在src/mongo/db/query/sbe/中实现核心类是PlanStage和CompiledExpression。SBE 使用 SSA静态单赋值形式的槽位表达式求值被编译为线性指令序列。大师总结查询执行引擎的三个核心——Plan Enumerator 生成候选计划 Stage 树执行物理查询 SBE 加速表达式求值。从 explain 到源码追踪是理解查询行为和性能差异的终极手段。3. 项目实战3.1 环境准备需要第 32 章搭建的 MongoDB 源码调试环境。3.2 分步实现步骤一在源码中找到 find 命令的入口// 源码追踪路径在 GDB 中打断点从外到内// 第 1 层命令入口// 文件src/mongo/db/commands/find_cmd.cpp// 函数FindCmd::run()// 作用解析 BSON 命令提取 filter/sort/projection/limit 等参数// 第 2 层获取查询执行器// 文件src/mongo/db/query/get_executor.cpp// 函数getExecutorFind()// 作用解析查询 → 规范化 → 生成 CanonicalQuery → 选择执行计划// 第 3 层计划生成// 文件src/mongo/db/query/plan_enumerator.cpp// 函数PlanEnumerator::enumerate()// 作用枚举所有可能的索引组合 排序策略// 第 4 层执行// 文件src/mongo/db/query/plan_executor_impl.cpp (Classic)// 或 src/mongo/db/query/sbe/stage_builder.cpp (SBE)// 作用构建 Stage 树并逐行执行# GDB 断点脚本——追踪 find 命令全链路 break mongo::FindCmd::run break mongo::getExecutorFind break mongo::PlanEnumerator::enumerate break mongo::PlanExecutorImpl::getNext步骤二观察候选计划能否通过目标理解索引选择逻辑——哪些索引能成为候选。// 用一个有多种索引的集合测试use local_life db.query_trace.drop()// 建多个索引让优化器有选择空间db.query_trace.createIndex({status:1,category:1,price:1})db.query_trace.createIndex({status:1,category:1,createdAt:-1})db.query_trace.createIndex({status:1,price:1})// 插入数据for(vari0;i50000;i){db.query_trace.insertOne({name:trace_i,status:i%100?下架:在售,category:[数码,家居,食品][i%3],price:i*1.5,createdAt:newDate(Date.now()-i*60000)})}// 分析查询计划的候选varexpdb.query_trace.find({status:在售,category:数码}).sort({createdAt:-1}).limit(20).explain(allPlansExecution)print( 优化器候选计划 )print(Winning:,exp.queryPlanner.winningPlan.indexName||COLLSCAN)varrejectedexp.queryPlanner.rejectedPlans||[]rejected.forEach(function(p,i){print(Rejected[i]:,p.indexName||COLLSCAN,p.stageSORT?(含SORT阶段):)})// 查看各候选的竞速扫描数if(exp.executionStats.allPlansExecution){exp.executionStats.allPlansExecution.forEach(function(p,i){print(计划i:,p.indexName||COLLSCAN,扫描键:,p.totalKeysExamined,扫描文档:,p.totalDocsExamined)})}步骤三理解 CanonicalQuery 的作用// CanonicalQuery 是优化器的输入——将查询标准化为统一的内部表示// 等价查询标准化// { status: { $in: [在售] } } → { status: 在售 }// { $and: [{a:1}, {b:2}] } → { a:1, b:2 }// { price: { $gt: 100, $gte: 100 } } → { price: { $gte: 100, $gt: 100 } }// 这步标准化在 getExecutorFind() 中完成// 源码中对应CanonicalQuery::canonicalize()// CanonicalQuery 还负责// - 提取等值条件Equality、排序Sort、范围Range的分类// - 计算查询的形状Query Shape用于 Plan Cache 匹配// - 判断是否能用覆盖索引Covered Query步骤四理解 Stage 树的执行模型// 以下用伪代码展示 Stage 树的 pull-based 执行模型/* class LimitStage { work() { if (returned limit) { auto child childStage-getNext(); // 向子 Stage 请求下一条 if (child) { result child; returned; return ADVANCED; // 返回一条结果 } return EOF; // 子 Stage 没有更多数据 } return EOF; // 已经返回够了 } } class SortStage { work() { // 第一阶段收集所有数据 排序 while (child-getNext()) { buffer.push_back(...); } sort(buffer); // 第二阶段逐条返回 return buffer.next(); } } class IxScanStage { work() { auto next indexCursor-next(); // 从索引中取下一个 key if (next) { return ADVANCED; } return EOF; } } */步骤五SBE 执行计划的观察// 查看当前使用的执行引擎varparamsdb.adminCommand({getParameter:1,internalQueryFrameworkControl:1})print(当前执行引擎:,params.internalQueryFrameworkControl)// trySbeEngine 默认优先 SBESBE 不支持的查询 fallback 到 Classic// forceClassicEngine 强制 Classic// SBE 的 explain 格式与 Classic 不同// SBE explain 的 stage 显示为 EXEC 而非经典 Stage 树// 用以下方式对比两者// Classic explain强制 Classic 引擎看db.adminCommand({setParameter:1,internalQueryFrameworkControl:forceClassicEngine})varclassicExpdb.query_trace.find({status:在售,category:数码}).sort({createdAt:-1}).limit(20).explain(executionStats)// 恢复 SBEdb.adminCommand({setParameter:1,internalQueryFrameworkControl:trySbeEngine})varsbeExpdb.query_trace.find({status:在售,category:数码}).sort({createdAt:-1}).limit(20).explain(executionStats)print(Classic:,JSON.stringify(classicExp.executionStats?.executionStages?.stage||?))print(SBE:,JSON.stringify(sbeExp.executionStats?.executionStages?.stage||?))print(Classic耗时:,classicExp.executionStats?.executionTimeMillis,ms)print(SBE耗时:,sbeExp.executionStats?.executionTimeMillis,ms)步骤六Plan Cache 的源码视角# GDB 中追踪 Plan Cache 的 hit/miss 行为# 打断点(gdb)breakmongo::PlanCache::get(gdb)breakmongo::PlanCache::add# 当 get 返回空 → Cache MISS → 触发 Plan Enumerator 竞速 → 调用 add 写入缓存# 当 get 返回非空 → Cache HIT → 直接使用缓存计划跳过竞速3.3 完整代码清单文件用途debug-scripts/find-chain.gdbGDB 脚本——find 全链路断点debug-scripts/query-trace-setup.js准备多索引测试数据debug-scripts/classic-vs-sbe.jsClassic 与 SBE 对比实验3.4 测试验证use local_life// 1. 验证 CanonicalQuery 标准化// 两种写法在 explain 中应有相同的 Plan Cache keyvarq1db.query_trace.find({status:{$in:[在售]}}).explain()varq2db.query_trace.find({status:在售}).explain()print(标准化: planCacheKey 相同?,q1.queryPlanner.planCacheKeyq2.queryPlanner.planCacheKey?PASS:注意观察)// 2. 验证 rejectedPlans 包含竞速数据varexpdb.query_trace.find({status:在售,category:数码}).sort({createdAt:-1}).limit(20).explain(allPlansExecution)print(候选计划数:,(exp.executionStats.allPlansExecution||[]).length)// 3. 验证 SBE vs Classic 切换db.adminCommand({setParameter:1,internalQueryFrameworkControl:forceClassicEngine})varcdb.query_trace.find({status:在售}).limit(5).explain(executionStats)print(Classic stage:,c.executionStats.executionStages.stage)db.adminCommand({setParameter:1,internalQueryFrameworkControl:trySbeEngine})varsdb.query_trace.find({status:在售}).limit(5).explain(executionStats)print(SBE stage:,s.executionStats.executionStages.stage)print(\n 查询引擎验证完成 )4. 项目总结4.1 查询执行链路速查阶段职责源码关键文件命令解析BSON → 内部查询对象find_cmd.cpp查询规范化等价变换、提取 ESR 分类canonical_query.cpp计划生成枚举索引组合 竞速选择plan_enumerator.cpp计划缓存缓存 winning plan 供后续复用plan_cache.cpp执行ClassicStage 树 pull 模型plan_executor_impl.cpp执行SBE槽位求值 VMsbe/stage_builder.cpp存储读取通过 RecordStore 读文档wiredtiger_record_store.cpp4.2 适用场景查询引擎源码追踪适用优化器选错索引的根因分析——是统计信息偏差还是竞速算法的近似缺陷。SBE 和 Classic 的行为差异排查——哪些查询在 SBE 下退化。自定义 MongoDB 功能——添加新的查询优化规则或聚合阶段。Plan Cache 命中率分析——缓存失效的触发条件。4.3 注意事项注意事项说明Plan Enumerator 的竞速不是全量执行每个候选计划只跑一小段选最快返回首屏的SBE 不是所有查询都支持复杂的地理空间查询、某些$lookup变体仍 fallback 到 ClassicPlan Cache 的失效条件索引变更、大量写入导致统计分布显著改变时失效强制internalQueryFrameworkControl仅调试用生产环境不应该强制切换执行引擎4.4 常见踩坑经验故障案例一SBE fallback 到 Classic 行为不同某团队从 MongoDB 4.4 升级到 5.0一个复杂聚合查询的 explain 从 COLLSCAN 变成了 IXSCAN。根因4.4 只有 Classic Engine5.0 的 SBE 对$project阶段有不同优化导致了不同的索引选择。解决在测试环境用forceClassicEngine对比确认行为差异对有差异的查询添加 hint 确保可预期的执行计划。故障案例二Plan Cache 导致性能退化某查询在数据分布变化后 Plan Cache 仍用旧计划totalKeysExamined 从 1000 膨胀到 10 万但优化器因为缓存命中不再重新竞速。解决db.collection.planCacheClear()手动清除或在查询上加 hint 后移除 hint让优化器重新竞速。故障案例三Sort Stage 在实际中比优化器认为的更贵优化器选了有 SORT 的计划因为它的竞速 trial period 还没到 SORT 阶段就开始返回数据——但全量执行时 SORT 把所有数据加载到内存排序导致 OOM。根因优化器对 SORT 的成本评估基于平均文档大小但如果实际文档很大内存排序的开销被低估。解决为需要排序的查询显式建覆盖排序需求的复合索引。4.5 思考题为什么 Plan Enumerator 不直接全量执行所有候选计划来选最优而是用竞速trial period的方式全量执行的代价有多大如果一个查询的 Plan Cache 命中但 explain 显示 totalKeysExamined 比预期多很多——是 Plan Cache 的问题还是统计信息的问题怎么区分答案将在第 35 章末尾揭晓上一章思考题答案缓存中 80% 是脏页的后果——Checkpoint 触发时需要刷入海量脏页可能导致数秒甚至数十秒的磁盘 IO 风暴期间应用层写入延迟飙升。MongoDB 默认限制脏页比例在 20% 以内——通过eviction_target和eviction_trigger参数当脏页超过 20% 时后台 eviction server 加速淘汰应用线程在脏页超过 95%eviction_dirty_target时被迫参与淘汰。j:true在 WiredTiger 层面调用WT_SESSION::log_flush()强制将当前 Journal 缓冲区刷入磁盘并等待fsync()完成——产生至少一次磁盘 IO数毫秒。而w:1只写内存缓存不发生磁盘 IO或由后台 100ms 的 sync 完成。这就是 j:true 比 w:1 慢 10-100 倍的原因。延伸阅读与资源MongoDB 实战进阶与内核修炼python入门Rquests从菜鸟脚本到企业级SDK的网络实战圣经Milvus向量数据库实战修炼从 0 到 1精通向量检索与生产落地后端工程师的 AI 转型第一课Ollama 与私有化大模型实战10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析

相关新闻

国产数据库全面替代Oracle?这可能就是最大的笑话!

国产数据库全面替代Oracle?这可能就是最大的笑话!

如果只看发布会,国产数据库可能已经赢了。性能领先、金融级高可用、分布式架构、平滑迁移、全面兼容、自主可控……每一个词都很先进,每一张架构图都很漂亮,每一组测试数据都足以让人热血沸腾。但如果走进真实的数据库迁移现场,看…

2026/7/24 9:32:08 阅读更多 →
第33章:MongoDB WiredTiger 存储引擎——从 B-Tree 到缓存淘汰

第33章:MongoDB WiredTiger 存储引擎——从 B-Tree 到缓存淘汰

1. 项目背景 业务场景:本地生活电商的 DBA 发现一个诡异的现象——高峰期 MongoDB 的磁盘 IOPS 飙升至 15000,但数据写入量明明只有 5000 IOPS。多出来的 10000 IOPS 是哪里来的?查看 WiredTiger 的统计指标后发现——pages evicted by appl…

2026/7/24 9:32:08 阅读更多 →
三大财务报表还不会看?资产、利润、现金流的分析顺序一次讲清

三大财务报表还不会看?资产、利润、现金流的分析顺序一次讲清

很多企业每个月都会出资产负债表、利润表和现金流量表,但管理层拿到报表后,往往只关注收入、净利润和银行余额。 结果就是: 利润增长了,却不知道钱为什么没有回来; 现金增加了,却分不清是经营赚来的&…

2026/7/24 9:32:08 阅读更多 →

最新新闻

南京中考信息合集(不定期更新)

南京中考信息合集(不定期更新)

仅供参考,涉及钱财和前途勿轻信 1.南京市教育局: https://edu.nanjing.gov.cn/zb/gzzs/index.html 2.南京教育之声,wx 3.南京魅力校园,wx 4.JSSzjgk,wx 5.苏通文创甄选,wx 6.老袁说中考,xhs 7.张老…

2026/7/24 9:40:12 阅读更多 →
昇腾NPU加速强化学习全异步训练方案解析

昇腾NPU加速强化学习全异步训练方案解析

1. 项目背景与核心价值去年在部署某金融风控系统时,我们团队第一次尝试将强化学习模型从实验室环境迁移到生产系统。当时面临的最大痛点就是训练效率问题——传统同步更新的RL训练方式在千万级状态空间下,单次迭代耗时高达47分钟。直到接触了全异步训练架…

2026/7/24 9:40:12 阅读更多 →
开源降AI率工具对比:千笔与知文的技术解析

开源降AI率工具对比:千笔与知文的技术解析

1. 开源降AI率平台的市场需求分析 在内容创作领域,AI生成内容(AIGC)的普及带来了效率革命,但同时也催生了新的需求——如何降低内容的"AI味"。根据我过去半年对12家内容平台的跟踪测试,平均有67%的编辑会在发…

2026/7/24 9:40:12 阅读更多 →
物理AI技术解析:从多智能体协同到实时控制优化

物理AI技术解析:从多智能体协同到实时控制优化

1. 物理AI的崛起与中国机遇 2026年世界移动通信大会(MWC)上最引人注目的现象,莫过于中国企业在物理AI(Physical AI)领域的集体爆发。这种将人工智能与物理世界深度融合的技术范式,正在从实验室走向产业化应用,而中国企业在这场变革中展现出令…

2026/7/24 9:39:12 阅读更多 →
软前缀技术增强大模型三段论推理稳定性:压力测试与优化实践

软前缀技术增强大模型三段论推理稳定性:压力测试与优化实践

在认知科学和自然语言处理交叉领域,逻辑推理任务一直是衡量模型智能水平的重要基准。当模型需要在压力条件下进行三段论推理时,其判断的稳定性会面临严峻挑战。这种压力可能来自时间限制、信息噪声或认知负荷增加,导致原本可靠的推理系统出现…

2026/7/24 9:39:12 阅读更多 →
大模型微调实战:从Llama2到ChatGLM的消费级GPU解决方案

大模型微调实战:从Llama2到ChatGLM的消费级GPU解决方案

1. 大模型技术全景与学习价值2023年被称为大模型技术爆发的元年,从Meta开源的Llama系列到清华智谱的ChatGLM,各类大型语言模型(LLM)正在重塑人工智能的应用范式。作为一名长期跟踪NLP技术演进的从业者,我观察到大多数学…

2026/7/24 9:39:12 阅读更多 →

日新闻

用Highcharts 创建可拖拽三维散点立方体3D图表

用Highcharts 创建可拖拽三维散点立方体3D图表

该案例基于Highcharts scatter3d 三维散点图实现空间立方体散点可视化,核心特色:三维 X/Y/Z 三轴空间,所有散点分布在 0~10 立方体空间内;散点使用径向渐变实现立体 3D 圆球质感;支持鼠标 / 触屏拖拽画布,…

2026/7/24 0:00:29 阅读更多 →
AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口

AppCertDlls:进程创建路径上的 DLL 入口 AppCertDlls 位于 HKLM\System\CurrentControlSet\Control\Session Manager\AppCertDlls。本文的程序功能是只读列出这个键在 64 位和 32 位注册表视图中的全部值,并显示每条值的来源、名称、类型和可安全显示的数…

2026/7/24 0:00:29 阅读更多 →
我的编程之路:第一篇博客

我的编程之路:第一篇博客

大家好,我是一名编程初学者,同时这也是我编程学习之路上的第一篇博客。在这里,我想要向大家介绍我的一些想法和规划。a.自我介绍我是一个刚刚接触编程的新手,目前在学习c语言,我对编程世界充满了强烈的好奇。当然&…

2026/7/24 0:00:29 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/24 3:59:20 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/24 1:23:39 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/23 17:49:47 阅读更多 →

月新闻