MongoDB explain 里 COLLSCAN、SORT 怎么排查?让 Codex 走 TaoToken 对着 executionStats 看
2026/9/18 10:49:32 网站建设 项目流程

在 mongosh 里执行db.products.explain("executionStats").find({ quantity: { $gt: 50 }, category: "apparel" })之后,返回的 JSON 常常长到一屏放不下,而真正让人卡住的不是字段数量,是眼睛先看到COLLSCANSORT,却不知道它们分别挂在哪一级阶段、该先动索引还是先动排序。MongoDB explain 的排障,更稳的顺序是先保存完整输出,再打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册 TaoToken、创建一把 API Key,把 Codex 接到 TaoToken 的模型通道,让 Codex 只根据你贴出的queryPlanner.winningPlanexecutionStats做逐项对照。TaoToken 只提供 Key 和 Base URL,不连接你的 MongoDB;查询、建索引、复跑 explain 都在本地 mongosh 完成。

1. 先复现:db.products.explain("executionStats") 输出里只盯三个位置

1.1 在 mongosh 里保存完整 explain 输出

排障最怕的是只截一张图,queryPlannerexecutionStats各看一半。先固定一个查询,把它完整跑出来。下面这条命令对应原文里executionStats模式的示例,只是把字段和格式改成更容易保存的形式:

use shop db.products.explain("executionStats").find({ quantity: { $gt: 50 }, category: "apparel" }).pretty()

如果怀疑优化器在多个计划之间犹豫,用allPlansExecution模式再看一次。原文也提到,只有这个模式才会在结果里加入allPlansExecution字段:

db.products.explain("allPlansExecution").update( { quantity: { $lt: 1000 }, category: "apparel" }, { $set: { reorder: true } } )

注意,explain 包裹的写操作不会真正修改文档,它只是让 MongoDB 把准备执行的计划暴露出来。把两次输出都存到本地文件,后面不管是自己对照,还是贴给 Codex 分析,都不用来回翻终端。

1.2 第一眼只看 queryPlanner.winningPlan.stage

queryPlanner里最重要的是winningPlan。它是一棵树:根节点是最终产出结果的阶段,中间节点处理子节点传来的文档或索引键,叶节点负责访问集合或索引。你看到COLLSCAN时,它通常出现在叶节点;你看到SORT时,它往往在根节点或靠近根节点的位置,说明排序没有交给索引完成。

不要一上来就逐字段读。先找winningPlan.stage,再顺着inputStage往下看。如果某一层出现COLLSCAN,基本可以确定这个分支没有走索引;如果某一层出现SORT,再回头看排序字段和已有索引的字段顺序是否对得上。

1.3 第二眼只看 executionStats 的 totalDocsExamined / totalKeysExamined

executionStats是给获胜计划补执行数据的。最值得先看的是:

  • nReturned:查询条件最终匹配到多少文档。
  • totalKeysExamined:扫描了多少索引键。
  • totalDocsExamined:扫描了多少文档。
  • executionTimeMillis:计划选择加执行的总耗时。

如果totalDocsExamined远大于nReturned,说明大量文档被扫出来又被过滤掉。如果totalKeysExamined很大而totalDocsExamined很小,可能是覆盖查询,也可能索引选择性不够。把这三个数字和阶段树放在一起看,比单独问“为什么慢”有用得多。

2. 阶段树拆解:COLLSCAN、IXSCAN、FETCH、SORT 谁是谁的父节点

2.1 叶节点、中间节点、根节点

MongoDB 把查询计划展开成阶段树。叶节点访问集合或索引,例如COLLSCANIXSCAN;中间节点对子节点产生的文档或索引键做过滤、取文档、合并;根节点是最终把结果集交给客户端的阶段。理解这一点之后,FETCH就不会再显得突兀:它通常是IXSCAN的父节点,表示先从索引拿到位置,再回集合取完整文档。

SORT的位置更值得盯。如果排序字段能被索引顺序满足,阶段树里通常不会出现SORT;一旦出现,说明 MongoDB 需要在内存里对结果重排。数据量小的时候看不出来,数据量一大,SORT加上COLLSCAN往往就是慢查询的组合拳。

2.2 常见阶段速查表

阶段含义典型位置排查关注
COLLSCAN集合扫描叶节点查询条件是否缺少可用索引
IXSCAN索引扫描叶节点索引边界、方向、字段顺序
FETCH回集合取文档中间节点是否可以通过覆盖查询去掉
SHARD_MERGE合并分片结果根附近分片集合才出现
SHARDING_FILTER过滤孤立文档中间节点分片集合才出现
LIMIT限制返回数量根附近能否让索引提前停止扫描
PROJECTION限定返回字段中间节点是否只返回必要字段
IDHACK_id精确查询叶节点通常很快
COUNTcount 运算根附近看是否退化成 COUNTSCAN
COUNTSCANcount 未用索引叶节点需要改成 COUNT_SCAN
COUNT_SCANcount 使用索引叶节点希望看到
SUBPLA未用索引的$or中间节点各分支分别看索引
TEXT全文索引查询叶节点全文索引场景
AND_SORTED/AND_HASH索引交集中间节点inputStages
OR$or使用索引中间节点看各分支是否 IXSCAN

2.3 希望看到与不希望看到的阶段

原文最后给出过一个很实用的清单:希望看到Fetch+IDHACKFetch+IXSCANLimit+Fetch+IXSCANPROJECTION+IXSCANSHARDING_FILTER+IXSCANCOUNT_SCAN。不希望看到COLLSCAN、无索引的SORT、不合理的SKIPSUBPLACOUNTSCAN

这个清单不是让你背术语,而是让你在 explain 输出里快速分类:看到IXSCAN先别高兴太早,继续看有没有FETCH和大totalDocsExamined;看到COLLSCAN也别立刻加索引,先确认查询条件、字段类型、排序和分页是否让索引失效。

3. queryPlanner.winningPlan 里 COLLSCAN 和 SORT 的定位方法

3.1 winningPlan、inputStage、inputStages、rejectedPlans

winningPlan是优化器选中的计划。只有一个子阶段时,用inputStage;有多个子阶段时,用inputStages。例如$or或索引交集会产生多个输入源。rejectedPlans是被拒绝的候选计划数组,没有其他候选时可能为空。

排查COLLSCAN时,先确认它出现在winningPlan还是rejectedPlans。如果只在rejectedPlans里,说明优化器没选它,问题不大;如果在winningPlan的叶节点,才需要继续深挖。排查SORT时,先看它是不是根节点,再看它的inputStageIXSCAN还是COLLSCAN。如果是IXSCAN但仍有SORT,大概率是索引字段顺序不满足排序。

3.2 出现 COLLSCAN 时先查什么

先看查询条件字段有没有索引。常见情况是categoryquantity都出现在查询里,但只给其中一个建了单字段索引,或者索引字段顺序和查询模式不匹配。再检查字段类型:字符串字段用数字去查,或者数字字段用字符串去查,都可能让索引用不上。

如果查询里有$or,优先看是否出现SUBPLASUBPLA表示$or的某些分支没有走索引。可以把每个分支单独 explain,确认哪个分支缺索引。如果查询里有正则表达式,也要注意前缀匹配和全文索引的区别。

3.3 出现 SORT 时先查什么

先看排序字段。假设查询按createdAt倒序,再按category过滤,那么索引里等值字段通常放在前面,排序字段放在后面。如果索引只有category,没有createdAt,排序就可能退化成内存SORT

再看分页。skip很大时,即使有索引,MongoDB 也可能需要扫描大量索引键再跳过。原文把“不合理的 SKIP”列入不希望看到的阶段,就是提醒你分页方式可能比索引本身更拖后腿。

4. executionStats:totalDocsExamined 和 totalKeysExamined 到底看哪个

4.1 nReturned、executionTimeMillis、totalKeysExamined、totalDocsExamined

executionStats描述获胜计划的完整执行信息。nReturned是最终返回数量,executionTimeMillis是计划选择加执行的总时间。totalKeysExamined是扫描的索引条目数,totalDocsExamined是扫描的文档数。

判断该看哪个,取决于阶段树。如果叶节点是COLLSCANtotalKeysExamined通常为 0 或很小,totalDocsExamined才是重点。如果叶节点是IXSCAN,先看totalKeysExamined是否接近集合总量,再看totalDocsExamined是否被FETCH放大。两个数字都大,说明索引选择性差且回表多。

4.2 executionStages 里的 keysExamined、docsExamined、works、advanced、needTime、isEOF

executionStages是带执行数据的阶段树。每个阶段可能有keysExamineddocsExaminedworksadvancedneedTimeneedYieldisEOFworks是工作单元数,advanced是返回给父阶段的结果数,needTime是没有产出中间结果的工作循环数,isEOF表示是否到达流末尾。

不要只盯着顶层。LIMIT阶段可能已经isEOF: 1,但底层IXSCAN仍然isEOF: 0。这说明查询虽然限制了返回数量,索引扫描并没有提前停止。把每层的keysExamineddocsExamined对齐看,才能找到真正的扫描来源。

4.3 覆盖查询与 FETCH:totalDocsExamined=0 意味着什么

如果IXSCAN不是FETCH的后代,并且totalDocsExamined为 0,通常说明索引覆盖了查询:MongoDB 只靠索引键就能匹配条件并返回结果,不需要回集合取文档。原文在兼容性修改里也提到,旧版本用indexOnly表示覆盖查询,新版本要看阶段树和totalDocsExamined

覆盖查询是优化方向之一,但不是唯一目标。如果为了覆盖查询把太多字段塞进索引,写入成本和索引体积也会上升。先解决COLLSCAN和大SORT,再考虑覆盖。

4.4 分片集合的 shards 输出别漏看

集合分片时,queryPlanner.winningPlan.shards会按分片列出计划信息,executionStats.executionStages.shards会按分片列出执行统计。不要只看顶层汇总,否则某个分片单独COLLSCAN或单独SORT会被平均掉。

serverInfo会给出 host、port、version、gitVersion。排查时顺手确认版本,因为 3.0 之后 explain 格式变化很大,旧文章里的cursornscannednscannedObjectsscanAndOrder已经对应到新字段。

5. 把 explain JSON 交给 Codex:TaoToken 的 Key 与 ~/.codex/config.toml

5.1 准备 Key:打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建

本地 explain 输出保存好之后,打开 TaoToken 注册并创建 API Key。Key 用占位符YOUR_API_KEY表示,不要把它写进文章、截图或公开仓库。模型 ID 不要从旧文章里抄,以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时列出的可用 ID 为准。

这里要分清楚两个地址:官网落地页用于注册、创建 Key、看模型广场、看用量;填进 Codex 的 Base URL 是https://taotoken.net/api,末尾不要加/v1,也不要填官网地址。

5.2 Codex 的 config.toml 写 Base URL = https://taotoken.net/api

Codex 的配置文件通常在~/.codex/config.toml。把自定义供应商指向 TaoToken 的 API 通道,示例写成这样:

# ~/.codex/config.toml model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"

然后让环境变量在当前终端生效:

export TAOTOKEN_API_KEY="YOUR_API_KEY"

Windows PowerShell 里可以这样:

$env:TAOTOKEN_API_KEY="YOUR_API_KEY"

YOUR_MODEL_ID换成模型广场里实际可用的模型 ID。base_url只写到https://taotoken.net/api,多写/v1或写成官网落地页都可能导致请求路径不对。配置完成后,Codex 负责解读你贴出的 explain 输出,TaoToken 只负责提供 Key 和 Base URL,不连接 MongoDB。

5.3 给 Codex 的提问模板:只贴 winningPlan 和 executionStats

不要对 Codex 说“你连上我的 MongoDB 跑一下”。它没有你的数据库连接,也不应该去连。正确的做法是先在本地 mongosh 执行,把输出贴回对话。提问可以按这个结构:

下面是我在本地 mongosh 执行 db.products.explain("executionStats").find({ quantity: { $gt: 50 }, category: "apparel" }) 得到的片段。 不要假设你能访问数据库。 queryPlanner.winningPlan: <粘贴 winningPlan> executionStats: <粘贴 executionStats> 请按阶段树逐层解释: 1. COLLSCAN 或 SORT 出现在哪一层; 2. 对照“不希望看到阶段”检查 SUBPLA、COUNTSCAN、不合理 SKIP; 3. 说明该重点看 totalDocsExamined 还是 totalKeysExamined; 4. 给出索引字段顺序建议和本地复跑命令。

这样问,Codex 的输出会围绕queryPlannerexecutionStats,而不是泛泛讲“加索引就好了”。

6. 本地复跑验证:IXSCAN、LIMIT、PROJECTION、COUNT_SCAN 有没有出现

6.1 调整索引后重新 explain

假设 Codex 对照输出后建议复合索引,先不要盲信,回到本地 mongosh 执行。示例:

db.products.createIndex({ category: 1, quantity: 1 })

然后重新跑同一条 explain:

db.products.explain("executionStats").find({ quantity: { $gt: 50 }, category: "apparel" }).pretty()

重点看winningPlan里是否出现IXSCANSORT是否消失,totalDocsExamined是否下降。如果排序字段不是quantity,索引尾字段要按实际排序调整,不要照抄。索引字段顺序错了,IXSCAN可能还是会出现,但SORT依旧在。

6.2 如果还是 COLLSCAN 或 SORT,继续让 Codex 对照 rejectedPlans

复跑后如果结果不理想,把新的winningPlanexecutionStatsrejectedPlans一起贴回对话,让 Codex 对比两次阶段树差异。尤其要指出rejectedPlans里有没有更优计划,以及优化器为什么没选它。这个对比过程比单次 explain 更有价值。

6.3 分片集合的验证不要只看一个分片

如果集合分片,复跑后要展开executionStages.shards,逐个分片看IXSCANCOLLSCANSORT。某个分片的数据分布可能让计划完全不同,顶层汇总看不出来。把分片名、阶段、totalDocsExamined一起贴给 Codex,让它按分片整理排查顺序。

7. SUBPLA、COUNTSCAN、不合理 SKIP 的专项排查

7.1 SUBPLA:$or 没用上索引

SUBPLA通常和未使用索引的$or有关。排查时把$or拆成几个单独查询,分别 explain,看哪个分支出现COLLSCAN。如果每个分支都有索引,但合起来还是SUBPLA,再看是否可以用复合索引或索引交集。原文里OR阶段和AND_SORTEDAND_HASH阶段都值得对照,重点看inputStages里每个分支是不是IXSCAN

7.2 COUNTSCAN 与 COUNT_SCAN

COUNTSCAN表示 count 没有使用索引,COUNT_SCAN表示 count 使用了索引。如果你在 explain 里看到COUNTSCAN,先确认查询条件字段是否有索引,再确认 count 是否被包装成了会扫描文档的形式。希望看到的阶段清单里明确列了COUNT_SCAN,所以 count 慢查询的优化目标很直接:让阶段树里出现COUNT_SCAN

7.3 skip 和分页

不合理的SKIP不一定单独显示成一个阶段,但它会体现在keysExamineddocsExamined上。翻到很后面的页时,MongoDB 可能已经跳过了大量索引键。可以考虑基于排序字段的范围分页,而不是单纯增大skip。这一点让 Codex 结合nReturnedtotalKeysExaminedtotalDocsExamined一起判断,比只看阶段名更准。

7.4 旧字段和新阶段名的对照

MongoDB 3.0 之后 explain 格式变化明显。旧版本cursor.explain()里的BasicCursorBtreeCursor <索引名>indexOnlyscanAndOrdernscannednscannedObjects等字段,分别对应新版本里的COLLSCANIXSCAN、覆盖查询、SORTtotalKeysExaminedtotalDocsExamined。如果你搜到的文章还在讲旧字段,贴给 Codex 时最好注明 MongoDB 版本,避免它按旧格式解释新输出。

8. 收尾:去控制台看这次 Codex 调用是否记上

配置完成后,先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息,确认模型 ID 和 Base URL 没填错。如果打算长期用 Codex 解读 explain、整理排查步骤,可以看 Coding Plan 是否够用;需要新建或轮换 Key,去 控制台 API Keys 创建。官网入口仍是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,模型广场和用量都从那里进。若你后面还把同一套通道接到 Claude Code,可对照 Claude Code 接入文档。回到 MongoDB 这边,把复跑后的winningPlan再贴回对话,比只问一句“为什么慢”有效得多。

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

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

立即咨询