MongoDB query_golden 黄金测试:防止部分索引的缓存计划被不合格查询复用
发布时间:2026/9/13 19:36:11 作者:尧图编辑部 阅读量:1,286

MongoDB query_golden 黄金测试防止部分索引的缓存计划被不合格查询复用【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo本文围绕 MongoDB 查询黄金测试框架query_golden中的一份预期输出文件 cached_partial_index_plan_not_reused_for_ineligible_query.md 展开完整解读它所固化的两个回归场景当一个可以使用部分索引partial index的查询把基于该索引的执行计划写入计划缓存plan cache之后一个查询形态相同但不合格ineligible的查询如何被保证不重新使用该缓存计划从而返回正确结果。读完后你将理解部分索引资格判定与计划缓存交互的边界条件以及 MongoDB 如何用黄金测试golden test机制对这类计划正确性回归做持续验证。测试背景这是一份黄金测试的预期输出文件首先要澄清该文件的性质jstests/query_golden/expected_output/sbeFull/cached_partial_index_plan_not_reused_for_ineligible_query.md 不是人工撰写的文档而是黄金测试框架自动比对的预期输出expected output。与之一一对应的测试脚本是 jstests/query_golden/cached_partial_index_plan_not_reused_for_ineligible_query_md.js其文件头部注释说明了测试目的Test that a query eligible for partial index and gets cached does not affect the results of a query with the same shape but ineligible for the partial index. This file is a regression testcase for SERVER-102825 and SERVER-106023.即这是一个针对 SERVER-102825 与 SERVER-106023 两个缺陷的回归用例。两个缺陷的共同点是计划缓存中缓存了某个合格查询的部分索引计划之后形态相同但不合格的查询错误地复用了该计划导致结果集错误。脚本借助 jstests/libs/query/pretty_md.js 提供的section/subSection/code辅助函数把每一步的查询、结果、explain 输出和计划缓存内容按 Markdown 小节组织输出最终落盘为预期输出文件。预期输出按查询引擎与特性开关的不同组合拆分为多个变体目录该测试共有四份默认变体sbeFull 变体本文主角featureFlagSbeFull 变体internalEnableJoinOptimization 变体从目录命名看sbeFull对应 SBEStaged Execution Engine引擎特性开关全量开启时的查询计划形态不同变体下 explain 细节可能不同但各变体都要固化同一组结果正确性 计划缓存内容断言。整个 query_golden 目录的测试运行方式与维护流程如何跑套件、如何看 diff、如何 accept 新计划在 jstests/query_golden/README.plan_stability.md 中有统一说明本文结尾会给出具体命令。脚本还引用了两个与断言直接相关的库函数见 测试脚本 顶部 importgetWinningPlanFromExplain来自 jstests/libs/query/analyze_plan.js从 explain 输出中截取 winningPlan写入文件即预期输出中的 Explain 小节outputPlanCacheStats来自 jstests/libs/query/golden_test_utils.jsdump 计划缓存条目collStats的planCache部分写入文件即 Plan cache 小节。这两个函数决定了预期输出文件的两大核心断言面计划本身与计划缓存条目。下面逐场景解读。场景一find 查询回归验证SERVER-102825预期输出文件的前半部分第 14 节对应脚本中的第一个代码块围绕一条集合上的一条find查询展开。1. 数据准备与无索引基线[ { _id : 1, a : 0 } ]集合中只插入一条文档{_id: 1, a: 0}。在创建任何用户索引之前先跑两条形态相同、仅$or第二分支边界值不同的查询建立基线查询谓词预期结果q1{$or: [{a: 1}, {a: {$lte: a string}}], _id: {$lte: 5}}[ ]空集q2{$or: [{a: 1}, {a: {$lte: 10}}], _id: {$lte: 5}}[ { _id : 1, a : 0 } ]基线阶段的 q1 返回空集、q2 返回唯一文档这一对照是后文一切断言的参照系正确行为下 q2 必须始终返回该文档。2. 创建部分索引 a_1{ a : 1, partialFilterExpression : { $or : [ { a : 1 }, { a : { $lte : a string } } ] } }partialFilterExpression与 q1 的$or部分完全一致。这意味着 q1 是合格eligible查询——其谓词能被部分过滤表达式所蕴含优化器可以为它生成走a_1索引的计划而 q2 的第二个分支$lte: 10不在该过滤表达式所能承诺的取值范围内属于不合格查询优化器不应为它选用这条部分索引。3. 计划入缓存explain 与 plan cache 断言脚本连续执行 3 次 q1注释写明 Ensure q1 is cached which uses the partial filter触发计划缓存随后固化两项内容Explain 小节——q1 的获胜计划预期输出 第 137–173 行{ stage : FETCH, planNodeId : 2, filter : { _id : { $lte : 5 } }, inputStage : { stage : IXSCAN, indexName : a_1, isPartial : true, keyPattern : { a : 1 }, direction : forward, indexBounds : { a : [ [1.0, 1.0], [\\, \a string\] ] } } }关键信息IXSCAN 走的是a_1isPartial : true由于谓词是$or扫描区间是两段——点区间[1.0, 1.0]对应分支a: 1范围区间[, a string]对应分支a: {$lte: a string}表示空串即字符串下界。_id: {$lte: 5}无法由单键索引a_1覆盖因此被下推为上层 FETCH 阶段的后置 filter。Plan cache 小节——预期输出 第 174–232 行 记录了缓存条目planCacheKey为E41825BBisActive: truecachedPlan即上面那个FETCH(部分索引 IXSCAN)计划createdFromQuery记录了入缓存的查询形状q1 的$or_id条件。这一步的意义在于黄金测试不仅断言结果正确还断言缓存里确实躺着这个基于部分索引的计划——只有缓存里确实有诱惑性计划后续 q2 的断言才构成对缺陷的有效回归。4. 核心断言q2 不得复用该缓存计划文件随后原样重放 q2{ $or : [ { a : 1 }, { a : { $lte : 10 } } ], _id : { $lte : 5 } }预期结果仍然是[ { _id : 1, a : 0 } ]。这正是回归点如果缺陷存在q2 与 q1 形状一致plan cache key 结构上极易命中同一条目可能直接复用缓存中的a_1部分索引计划而a_1只索引了满足部分过滤表达式的文档复用它会漏掉不满足过滤表达式但仍应被查询命中的文档导致 q2 错误地返回空集。黄金文件把q2 返回正确文档固化为可自动比对的断言。场景二聚合管道回归验证SERVER-106023预期输出文件的第 5 节对应脚本第二个代码块把同一类问题放到聚合管道$match$sort与多索引并存的情境中复现。1. 三个部分索引同样的单文档集合[ { _id : 1, a : 0 } ]上创建三条索引共享同一部分过滤表达式{ $or : [ { a : { $lt : a } }, { _id : { $eq : 0 }, a : { $eq : 0 } } ] }索引键说明{ a : 1 }单键{ a : 1, m : 1 }双键引入m前缀干扰{ b : 1, a : 1 }双键为后续$sort: {b: 1}提供排序候选三条索引同时存在使优化器在多候选计划间做选择扩大了回归覆盖面无论哪条索引的计划先入缓存不合格管道都不应误用。2. 管道 p1合格计划入缓存[ { $match : { $or : [ { a : { $lt : a } }, { _id : { $eq : 0 }, a : { $eq : -1 } } ] } }, { $sort : { b : 1 } } ]脚本注释标明$or第一分支a: {$lt: a}This branch matches the partial filter即 p1 合格可走部分索引。连续执行 3 次后 dump 计划缓存预期输出 第 438–533 行 固化了条目7949C151isActive: true其cachedPlan是一个典型的 OR 扫描结构{ stage : SORT, memLimit : 104857600, sortPattern : { b : 1 }, inputStage : { stage : FETCH, inputStage : { stage : OR, inputStages : [ { stage : FETCH, filter : { a : { $eq : -1 } }, inputStage : { stage : IXSCAN, indexName : _id_, indexBounds : { _id : [ [0.0, 0.0] ] } } }, { stage : IXSCAN, indexName : a_1, isPartial : true, indexBounds : { a : [ [\\, \a\) ] } } ] } } }结构解读外层SORT完成$sort: {b: 1}memLimit104857600 字节即 100MB 内存排序上限OR 扫描的两个分支中第一分支走唯一索引_id_的点扫描[0, 0]a: {$eq: -1}作为 FETCH 后置过滤第二分支走部分索引a_1isPartial: true区间[, a)正是$lt a的开区间表示。注意第二条分支没有任何索引可覆盖第二谓词时优化器选择唯一索引定位 后置过滤而非部分索引——这说明缓存计划中部分索引只是 OR 的一支误复用的风险面比场景一更大。3. 核心断言p2 不得复用该缓存计划第二条管道与 p1 同形但把合格分支换到了$or的另一支[ { $match : { $or : [ { a : { $lt : 1 } }, { _id : { $eq : 0 }, a : { $eq : 0 } } ] } }, { $sort : { b : 1 } } ]脚本注释标明第二分支_id: 0, a: 0This branch matches the partial filter而第一分支a: {$lt: 1}的数值边界落在部分过滤表达式a: {$lt: a}的承诺范围之外因此 p2 整体不合格。预期结果必须是[ { _id : 1, a : 0 } ]a: 0满足a: {$lt: 1}。若 p2 错误复用了 p1 缓存的部分索引计划文档将被漏掉、返回空集——与场景一同一类数据正确性缺陷只是发生在$match下推到索引扫描的聚合路径上。机制解读为什么不合格查询复用部分索引计划是严重缺陷综合两个场景可以从源码与测试结构看到三层防护逻辑也是理解该黄金测试价值的框架部分索引只包含合格文档。partialFilterExpression决定哪些文档被收录进索引。对不合格查询而言未收录文档中仍可能有命中者如{_id: 1, a: 0}之于 q2/p2任何只依赖部分索引内容的计划都会漏文档。计划缓存以查询形状为 key。q1/q2、p1/p2 的形状高度同构缓存条目极易被后续查询命中。缺陷SERVER-102825、SERVER-106023即出在命中后缺少该查询对缓存计划中的部分索引是否合格的复核直接执行了含isPartial: trueIXSCAN 的旧计划。黄金测试用三重固化做回归基线结果无索引时 q1/q2、以及 q2/p2 的最终结果保证结果正确性explain 固化getWinningPlanFromExplain保证合格查询确实选用了部分索引计划plan cache 固化outputPlanCacheStats含planCacheKey与isActive保证存在诱惑性缓存条目这一缺陷前提真实成立。三者缺一回归用例都不可靠没有第三项缺陷复现前提可能不成立没有第一项无法证明最终结果正确。这也是 query_golden 框架的典型用法——它不只是快照查询计划而是把缓存内容 查询 结果作为一个整体不变量来守护。如何运行与维护该测试运行方式沿用 jstests/query_golden/README.plan_stability.md 描述的黄金测试流程。以 classic 引擎套件为例该 README 给出的标准形式buildscripts/resmoke.py run \ --suitesquery_golden_classic \ jstests/query_golden/cached_partial_index_plan_not_reused_for_ineligible_query_md.jsREADME 还列出了若干预定义套件如query_golden_cbr_automatic、query_golden_cbr_sampling等分别对应不同计划排序模式可直接--suitessuite使用。测试失败时可用 buildscripts/golden_test.py 的diff子命令查看预期与实际输出的差异、用accept子命令接受新计划README 中给出了定制 git diff 属性**/plan_stability* diffplan_stability以获得按 pipeline 分段的 diff 输出的完整步骤。若只想人工排查可按 README 的--pauseAfterPopulate流程把数据与索引准备好后暂停测试再用 mongosh 连接mongodb://127.0.0.1:20000对本测试的集合db.cached_partial_index_plan_not_reused_for_ineligible_query_md集合名即测试名见 explain 输出中的nss字段手工执行两条查询/管道并用explain观察计划与计划缓存行为。适用前提与限制该预期输出绑定当前仓库版本下的计划形态与 plan cache key 值如E41825BB、7949C151不同引擎/特性开关组合应查看对应的预期输出变体目录跨变体比较计划细节没有意义但各变体中的结果集断言q2/p2 必须返回文档是版本无关的正确性底线。运行需要本仓库构建出的 mongod 与 resmoke 环境buildscripts/resmoke.py、buildscripts/uv_sync.sh 等 Python 依赖准备步骤见仓库贡献与开发文档。本文所有计划结构、索引区间、缓存条目均以 sbeFull 变体预期输出 与 测试脚本 的实际内容为准如优化器行为演进应通过黄金测试的 diff/accept 流程审视变更而非直接修改预期文件。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考