必须显式指定读取偏好为primary,否则explain("executionstats")在副本集上会因路由到secondary而报错;因secondary禁止执行需运行时统计的操作,而queryplanner不执行查询、无法反映真实性能瓶颈。

直接结论:在副本集上执行 explain("executionStats") 聚合查询,必须显式指定读取偏好为 Primary,否则可能因路由到从节点而报错或返回不完整统计。
为什么聚合 explain 在副本集上容易失败
mongos 或副本集客户端默认可能将 explain 请求发往从节点(Secondary),但 explain("executionStats") 需要真实执行查询并收集耗时、扫描数等运行时指标——而从节点默认禁止写入类操作(包括临时游标创建和统计采集),会直接返回错误:
CommandNotSupportedOnSecondary: explain is not supported on secondary nodes
这不是权限问题,是 MongoDB 内部对 Secondary 的硬性限制。即使你用 readPreference=secondaryPreferred 连接,explain("executionStats") 也必须走主节点。
正确执行聚合 explain 的三步操作
以 Go 驱动(v1.14+)为例,关键不是改连接字符串,而是为单次命令显式控制路由:
- 构造聚合管道时,仍用标准
collection.Aggregate()方式,但不要直接执行; - 改用
db.RunCommand()手动拼explain命令,并传入options.RunCmd().SetReadPreference(readpref.Primary()); - 命令文档结构必须是
bson.D,且顶层键为"explain",值为原始聚合命令对象(含aggregate、pipeline、cursor等字段)。
示例代码片段:
command := bson.D{
{"explain", bson.D{
{"aggregate", "orders"},
{"pipeline", bson.A{bson.D{{"$match", bson.D{{"status", "shipped"}}}}}},
{"cursor", bson.D{}},
}},
}
opts := options.RunCmd().SetReadPreference(readpref.Primary())
var result bson.M
err := db.RunCommand(context.TODO(), command, opts).Decode(&result)
explain 模式选 "executionStats" 而非 "queryPlanner"
在副本集调优场景下,只看 queryPlanner 不够——它不执行查询,无法暴露实际瓶颈:
-
queryPlanner:仅模拟选索引逻辑,返回winningPlan和rejectedPlans,但nReturned、executionTimeMillis全为 0; -
executionStats:真跑一次,给出totalDocsExamined(扫了多少文档)、totalKeysExamined(用了多少索引键)、executionTimeMillis(真实耗时),这才是定位慢聚合的关键; -
allPlansExecution:极少需要,开销大,且副本集主节点压力敏感,不建议日常使用。
分片集群中聚合 explain 的额外注意点
如果你实际连的是 mongos(而非直连副本集),explain 返回的 executionStats 里会多一个 splitPipeline 字段,它告诉你聚合是否被下推到各分片执行。重点看:
-
mergeType字段值:若为"mongos",说明合并阶段在路由层完成,网络传输和内存压力都在mongos; -
shards数组长度:等于实际参与查询的分片数,若远小于集群总分片数,说明查询带了分片键,属定向操作; - 每个
shards子项里的executionStats:必须逐个检查,某一分片的executionTimeMillis显著偏高,往往意味着该分片数据倾斜或本地索引缺失。
真正容易被忽略的是:即使你在 mongos 上执行 explain,它返回的 executionTimeMillis 是端到端总耗时,不包含 mongos 合并结果的时间——这部分开销藏在 serverInfo 或需用 db.currentOp() 结合 mongos 日志交叉验证。











