)
MongoDB 查询黄金测试深度解析大数组 $in 谓词下的最优索引选择以 large_in_with_indexes 为例【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo导读jstests/query_golden/expected_output/large_in_with_indexes.md是 MongoDB 查询优化器Query Planner黄金测试Golden Test的一份期望输出文件当一条携带 300 个_id值的$in过滤条件、叠加rd范围过滤并按ard排序的查询在带有多字段复合索引的集合上执行时优化器应如何选择执行计划。本文以该文件为骨架结合其测试驱动脚本jstests/query_golden/large_in_with_indexes_md.js与 golden 测试框架逐字段解读执行计划 JSON、索引边界indexBounds的生成逻辑并说明如何在本地复现、验证与维护此类计划稳定性测试。测试场景一次大 $in 复合索引的计划选择实验数据集与索引布局驱动脚本jstests/query_golden/large_in_with_indexes_md.js构造了如下实验环境数据全部随机生成固定随机种子1保证输出可复现集合test.large_in_with_indexes共 100000 条文档每条文档形如{_id: i, rd: 0~99999 随机整数, ard: 0~99999 随机整数}依次创建四个索引单字段rd: 1、单字段ard: 1、复合索引{rd: 1, ard: 1}、复合索引{_id: 1, rd: 1, ard: 1}待测查询的过滤条件与排序为过滤{rd: {$gte: 7}, _id: {$in: 300 个随机 _id}}排序{ard: 1}该查询同时包含等值集合_id的$in、范围谓词rd 7与排序键ard三类要素是考察优化器在先过滤再排序与利用索引覆盖排序之间权衡的典型构造。对应测试脚本位于jstests/query_golden/large_in_with_indexes_md.js其注释明确本测试的意图是Tests that a query with $in filter over a large array chooses the optimal index.期望执行计划逐层解读文档jstests/query_golden/expected_output/large_in_with_indexes.md记录的获胜计划winning plan由三层 Stage 组成自底向上为IXSCAN - SORT - FETCH并附有一句关键说明Note: expecting an IXSCAN on_id_1_rd_1_ard_1IXSCAN索引扫描的选择与边界计划底部是索引扫描节点其关键字段如下节选自期望输出 JSON{ stage: IXSCAN, nss: test.large_in_with_indexes, keyPattern: {_id: 1, rd: 1, ard: 1}, indexName: _id_1_rd_1_ard_1, isMultiKey: false, multiKeyPaths: {_id: [], rd: [], ard: []}, isUnique: false, isSparse: false, isPartial: false, direction: forward, indexBounds: { _id: [[355.0, 355.0], [587.0, 587.0], [1310.0, 1310.0], ...], rd: [[7.0, inf]], ard: [[MinKey, MaxKey]] } }各字段含义字段值含义keyPattern{_id: 1, rd: 1, ard: 1}被选中的复合索引键模式说明优化器在四个候选索引中选择了_id_1_rd_1_ard_1indexName_id_1_rd_1_ard_1MongoDB 自动生成的索引名字段_方向拼接isMultiKeyfalse索引不含数组字段不是多键索引multiKeyPaths三个空数组与isMultiKey呼应逐一列出每个索引字段上产生多键的路径isUnique/isSparse/isPartial均为false普通索引非唯一、非稀疏、非部分索引directionforward正向扫描升序indexBounds谓词如何映射为索引边界indexBounds是这份文档中最有价值的信息它直接展示了谓词 - 扫描区间的转换结果且各字段含义不同_id由$in数组中的 300 个值逐一生成300 个点区间point interval每个值形成一个[值, 值]的闭区间例如[355.0, 355.0]、[587.0, 587.0]。这正是文档中长达 300 个边界条目列表的来源——每个$in元素都被索引边界求值器IndexBoundsBuilder翻译成独立的扫描区间因此$in数组越大产生的点区间越多索引扫描的 seek 次数也随之线性增长rd{$gte: 7}被转换为半开右区间[7.0, inf]其中inf表示正无穷ard由于该字段仅出现在排序键sort: {ard: 1}中、未参与任何过滤谓词其边界为全开区间[MinKey, MaxKey]——意味着索引对该字段不施加任何筛选仅用作后续排序的数据来源。从索引键顺序_id - rd - ard可以印证优化器的决策$in的等值集合放在最左前缀rd的范围谓词作为第二键ard保留在索引末尾以提供排序键覆盖。这种布局使得扫描出的记录天然按ard有序进而可以直接复用索引序完成排序见下文 SORT 节点。SORT排序节点与内存上限{ stage: SORT, sortPattern: {ard: 1}, memLimit: 104857600, type: default }sortPattern为{ard: 1}与查询的.sort({ard: 1})一致memLimit为 104857600 字节100 MiB即 MongoDB 单次排序允许使用的内存上限超过后会将部分数据溢出到磁盘type为default表示常规的基于内存/溢出排序而非sort_merge等特殊策略。值得注意的细节是期望计划中的 SORT 位于 IXSCAN 之上、FETCH 之下说明此处的排序并未直接利用索引顺序否则$sort会与 IXSCAN 合并、explain中不会出现独立 SORT 节点而是将 IXSCAN 输出的键值在内存中重新排序后再回表 FETCH 取回完整文档。这同时说明了_id_1_rd_1_ard_1索引在该查询中主要服务于$in的等值定位与rd的范围裁剪而非排序消除。FETCH回表取文档{ stage: FETCH, nss: test.large_in_with_indexes }IXSCAN 只扫描索引键不包含完整文档FETCH 阶段根据索引条目中的_id定位并读取完整的 BSON 文档最终输出满足过滤条件的记录。由于$in中的值几乎都满足rd 7命中率接近 100%见下文输出统计FETCH 的代价主要由 300 个点区间扫描的 seek 次数决定。为什么不是其他索引测试脚本共创建了 4 个索引其中_id_1_rd_1_ard_1是唯一同时满足以下条件的候选最左前缀_id可以直接承接$in的 300 个等值点区间第二键rd可以继续承接{$gte: 7}的范围谓词第三键ard与排序键一致作为可选的排序辅助。而{rd: 1}、{ard: 1}与{rd: 1, ard: 1}要么缺失_id等值前缀无法直接做点定位要么无法同时兼顾范围谓词与排序键。从源码结构可以推断MongoDB 查询优化器在多方案枚举multi-planning阶段会比较各索引的代价估计最终选择_id_1_rd_1_ard_1作为获胜计划——这正是 golden 测试所要锁定的计划稳定性行为。查询输出与计划正确性文档末尾的 Output 部分记录了实际查询返回的记录列表每条形如{ _id: 2483, rd: 79880, ard: 164 }通过对比可验证三个正确性要点过滤正确返回记录的rd均满足 7最小值即过滤下界附近且_id全部落在 300 个$in值集合内排序正确返回记录的ard严格单调递增164、189、455、699、1117……与sort: {ard: 1}一致结果完整从计划推断300 个点区间在rd 7过滤下几乎全部命中输出条目数与命中记录数吻合无遗漏。这意味着期望计划 期望输出共同构成了对优化器既选对计划、又算出正确结果的双重校验即使某次改动让执行计划保持不变只要输出发生偏移同样会被 golden 测试捕获。该文档在 golden 测试体系中的角色期望输出如何产生large_in_with_indexes.md属于jstests/query_golden/expected_output/目录下的期望输出expected output。驱动脚本通过jstests/libs/query/pretty_md.js提供的section、subSection、code、codeOneLine等辅助函数将查询计划与结果以统一的 Markdown 格式写出例如section(Large Indexed $in)生成## 1. Large Indexed $in对应文档第 1 节标题编号由框架自动累加codeOneLine({filter, sort})生成反引号包裹的单行过滤/排序条件subSection(Expected plan)生成### Expected plan随后code(tojson(normalizePlan(...)))输出规范化后的获胜计划 JSONsubSection(Output)与codeOneLine(output)输出查询结果。因此本文关联文档的 Markdown 结构##小节、###子节、json 代码块、反引号单行 JSON全部由测试框架自动渲染而非人工手写这保证了格式的稳定与可对比性。与其他变体的关系同一测试在jstests/query_golden/expected_output/internalEnableJoinOptimization/large_in_with_indexes.md下还有一份变体期望输出其差异仅在于计划节点中多出usedJoinOptimization: false字段——即开启内部 Join 优化internalEnableJoinOptimization后该查询仍未被改写为 Join 形式执行计划与默认路径一致。这说明该查询属于Join 优化不适用的场景两份期望文件共同锁定了不同优化开关下的计划稳定性。计划稳定性测试的定位依据jstests/query_golden/README.plan_stability.mdplan_stability系列测试记录了一批查询的当前获胜计划一旦计划发生变化测试即失败由人工判断计划变更是否合理。large_in_with_indexes_md.js属于同类 query golden 测试它把大$in查询应使用_id_1_rd_1_ard_1索引固化为期望行为任何导致优化器改选其他索引或改变边界的改动都会立即暴露。本地复现与运行query golden 测试是标准 golden 测试可通过 resmoke 运行。以计划稳定性测试为例jstests/query_golden/README.plan_stability.md提供了通用运行方式buildscripts/resmoke.py run \ --suitesquery_golden_classic \ jstests/query_golden/plan_stability.js针对计划排序器Plan Ranker的不同模式仓库预置了多个专用 suite例如buildscripts/resmoke.py run --suitesquery_golden_cbr_automatic jstests/query_golden/plan_stability.js若要显式指定查询框架与计划排序参数可仿照jstests/query_golden/README.plan_stability.md中的示例组合buildscripts/resmoke.py run \ --suitesquery_golden_classic \ --mongodSetParameters{internalQueryFrameworkControl: forceClassicEngine, featureFlagCostBasedRanker: ..., internalQueryPlanRanker: ..., internalQueryCBRCEMode: ...} \ jstests/query_golden/plan_stability.js手动验证复现同样的计划与输出不依赖 resmoke也可以在mongosh中手动复现本文场景对照期望文件逐项验证const coll db.large_in_with_indexes; coll.drop(); Random.setRandomSeed(1); // 构造 100000 条随机文档 const docs []; const numDocs 100000; for (let i 0; i numDocs; i) { docs.push({_id: i, rd: Random.randInt(numDocs), ard: Random.randInt(numDocs)}); } // 创建 4 个索引与测试脚本一致 coll.createIndex({rd: 1}); coll.createIndex({ard: 1}); coll.createIndex({rd: 1, ard: 1}); coll.createIndex({_id: 1, rd: 1, ard: 1}); coll.insertMany(docs); // 生成 300 个随机 _id const inArray []; for (let i 0; i 300; i) { inArray.push(Random.randInt(numDocs)); } // 查询并查看执行计划 const filter {rd: {$gte: 7}, _id: {$in: inArray}}; const sort {ard: 1}; coll.find(filter).sort(sort).explain(executionStats).queryPlanner.winningPlan; // 验证输出 coll.find(filter).sort(sort).toArray();对照要点winningPlan应为FETCH - SORT - IXSCAN三层结构且 IXSCAN 的indexName为_id_1_rd_1_ard_1indexBounds._id应为 300 个[值, 值]点区间rd为[7.0, inf]ard为[MinKey, MaxKey]返回结果的ard应严格升序且_id均在$in集合内、rd均不小于 7。若某一步出现偏差例如 IXSCAN 选中了rd_1_ard_1索引、或$in边界被合并成区间说明优化器行为发生了变更需要回到 golden 测试流程中判断变更好坏。维护提示接受新计划当计划变更被人工评估为合理更优时使用buildscripts/golden_test.py accept接受新期望输出其他 golden 测试同理展示失败 diff按jstests/query_golden/README.plan_stability.md配置~/.golden_test_config.yml的diffCmd与$HOME/.config/git/attributes后可用buildscripts/golden_test.py diff获得逐计划差异包括变更的winningPlan、indexName、indexBounds及执行计数器评估计划优劣对于$sort、$limit类计划计数器可能好坏参半可在 mongosh 中通过setParameter开关featureFlagCostBasedRanker对比同一查询在经典计划与 cost-based 计划下的executionTimeMillis再作判断详细步骤见jstests/query_golden/README.plan_stability.md。总结jstests/query_golden/expected_output/large_in_with_indexes.md以一份可读性极强的 Markdown 期望输出完整记录了 MongoDB 查询优化器在面对300 值$in 范围谓词 排序键组合时做出的计划决策选用_id_1_rd_1_ard_1复合索引将$in展开为 300 个点区间、rd裁剪为[7, inf]、ard交由排序节点处理并返回严格按ard升序的结果。理解这份文件的每一层 Stage 与indexBounds语义不仅能读透 query golden 测试的输出格式也能直接迁移到真实业务的索引设计判断当$in数组规模增大时最左前缀的选择、点区间的数量与排序内存上限都是需要提前评估的优化变量。【免费下载链接】mongoThe MongoDB Database项目地址: https://gitcode.com/GitHub_Trending/mo/mongo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考