索引前缀匹配需等值字段在复合索引最左且精确匹配,范围字段紧随其后、不可跳过;$match须置于$lookup等阶段之前;副本集从节点explain结果虽同主节点,但数据延迟会影响实际扫描量。

用 explain 查看索引是否命中前缀匹配
直接在副本集的主节点上运行带 {explain: true} 的聚合命令,重点看 executionStats.executionStages 下的 stage 类型和 indexName 字段。如果出现 "stage": "COLLSCAN" 或 "stage": "IXSCAN" 但 indexName 是你没预期的索引,说明前缀不匹配。
常见错误现象:查询 {"created_at": {"$gte": ISODate("...")}, "status": "done"} 却建了 [("status", 1), ("created_at", -1)] 索引——MongoDB 只能利用等值字段(status)部分,范围字段(created_at)无法生效,实际只用了单字段前缀。
- 等值字段必须严格放在复合索引最左侧,且查询中必须是
$eq、$in这类精确匹配 - 范围字段(
$gt、$lt、$regex等)只能跟在等值字段之后,且不能跳过中间字段 - 排序字段若参与索引,必须放在范围字段之后(如
[("a", 1), ("b", 1), ("c", -1)]支持{a: 1, b: {$gt: 2}}+.sort({c: -1}))
验证副本集从节点能否复用相同执行计划
副本集从节点默认禁用写操作,但 MongoDB 5.0+ 允许在满足条件时让含 $merge 或 $out 的聚合在从节点执行(只读阶段仍可跑)。不过索引匹配逻辑与主节点一致,**关键区别在于:从节点的 oplog 应用延迟可能导致数据新鲜度不同,explain 结果虽一致,但实际扫描文档数可能偏高**。
使用场景:你在从节点做性能预估时,必须确认该节点已同步到目标时间点的数据快照,否则 totalDocsExamined 偏低会误导判断。
- 先查
rs.printSecondaryReplicationInfo()确认延迟(syncingTo和optimeDate) - 在从节点执行
explain前,加{readPreference: {mode: "secondary", tagSet: [...]}}显式指定 - 避免在延迟 > 1s 的从节点上评估索引效果,尤其涉及时间范围查询时
聚合管道里 $match 位置错位导致前缀失效
即使索引结构正确,$match 放在 $lookup 或 $unwind 后面,也会让 MongoDB 先加载大量无关文档,索引只在第一阶段生效,后续阶段完全脱离索引控制。
典型错误示例:[{"$lookup": {...}}, {"$match": {"status": "done"}}] —— 此时 $match 对左表无效,右表又没索引,全量关联后才过滤,内存暴涨。
-
$match必须放在所有产生新文档的阶段($lookup、$unwind、$facet)之前 - 若需在
$lookup后过滤右表,必须把$match写进pipeline参数里,例如{"$lookup": {"from": "orders", "pipeline": [{"$match": {"paid_at": {"$gt": t}}}], "as": "paid_orders"}} - 聚合开头的
$match若未命中索引前缀,整个管道都失去优化基础
用 db.collection.getIndexes() 检查索引定义是否被误删或覆盖
副本集各节点索引元数据一致,但运维过程中可能有人在某个节点手动删了索引,或通过 createIndex 覆盖了旧索引却忘了同步。此时 explain 在主节点看到索引名,但从节点实际缺失,执行计划会退化为全表扫描。
容易踩的坑:索引名默认由字段顺序+方向生成(如 status_1_created_at_-1),但手动指定 name 后,getIndexes() 输出的名称可能和你记忆不符,导致误判。
- 执行
db.orders.getIndexes()对比所有节点输出,确认索引存在且字段顺序一致 - 检查
key字段是否完全匹配,注意1和-1方向差异会影响排序兼容性 - 索引状态为
"ready"才可用;若为"building"或"failed",需重建
totalDocsExamined 异常,但生产环境某字段取值倾斜(比如 95% 文档 status === "pending"),会让看似合理的索引前缀实际失效**。务必在同分布数据上验证。











