MongoDB 的索引行为和关系型数据库并不完全一致。不少人在字段上建了索引,但因为查询写法或数据类型问题,优化器仍然选择了 COLLSCAN。要弄清楚索引为什么失效,首先要理解 MongoDB 的查询计划选择逻辑。优化器不会无条件使用某个索引,它会根据查询条件、排序字段以及索引前缀的匹配程度估算代价。只要查询形状和索引定义出现偏差,索引就可能被静默放弃。

比较常见的误区是:只要给字段建了索引,所有涉及该字段的查询都应该走索引。实际上,MongoDB 对查询条件非常敏感,字段类型、操作符种类、复合索引字段顺序都会影响索引选择。下面先梳理最容易导致索引失效的几个触发条件,再用执行计划验证,最后给出可落地的整改方案。
一、索引失效的常见触发条件
MongoDB 优化器在选择索引时,会优先考虑能够缩小扫描范围的查询形状。以下条件容易让索引失去优势,甚至完全无法命中索引。
- 字段类型不一致:索引中保存的是 BSON 类型信息,字符串类型的
25和数字类型的25在索引里是完全不同的值。如果查询值类型与索引字段类型不匹配,MongoDB 不会进行隐式类型转换,而是直接放弃索引。 - 正则表达式没有左前缀:
/^张/可以利用索引缩小范围,但/张/、/张/i这类无法确定起始范围的模式,只能扫描整个集合。 - 否定条件过多:
$ne、$nin、$not通常需要排除大量文档,索引带来的收益很低,优化器大概率会选择全表扫描。 - 多分支 $or:每个
$or分支都必须有独立索引支持,否则整体查询会退化为 COLLSCAN。 - 复合索引前缀错位:使用
{ status: 1, createdAt: -1 }这样的复合索引时,跳过status直接查询createdAt,索引不会被使用。 - 范围条件与排序冲突:复合索引中,范围查询字段之后的排序字段通常无法继续利用索引排序。
以类型不一致为例,假设集合中部分文档的 age 字段被写成了字符串:
db.users.insertMany([
{ name: "张三", age: 25 },
{ name: "李四", age: "25" }
])
db.users.createIndex({ age: 1 })
db.users.find({ age: 25 }).explain("executionStats")
执行后观察 winningPlan.stage,很可能是 COLLSCAN。原因是存在字符串类型的 age 时,优化器无法保证索引覆盖全部查询结果,必须回到集合中逐条核对。即使只有少量脏数据,也会影响优化器对索引收益的判断。
复合索引前缀错位也很隐蔽。比如订单表创建了 { status: 1, createdAt: -1 },查询只写 createdAt 条件时,索引无法使用。而查询 { status: "paid", createdAt: { $gte: startDate } } 时,两个字段都能参与索引范围扫描。理解 B-tree 索引的排序规则是避免这类问题的关键。
二、用 explain 核查执行计划
排查索引是否失效,最直接的方式是查看执行计划。explain("executionStats") 返回的信息中,重点看三个字段:winningPlan.stage、totalKeysExamined 和 totalDocsExamined。stage 为 IXSCAN 表示走了索引,COLLSCAN 则是全表扫描。如果 totalDocsExamined 远大于 totalKeysExamined,说明索引虽然被使用但过滤性很差,仍然需要回表检查大量文档。
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
db.orders.find({
status: "paid",
createdAt: { $gte: startDate }
})
.sort({ amount: -1 })
.explain("executionStats")
执行计划中还要关注 rejectedPlans,里面记录了优化器放弃的候选索引。如果某个预期索引出现在被拒绝列表中,可以进一步查看 rejectedPlans 的 stage 和代价估算,判断放弃原因。有时只是因为统计信息过期,执行 db.collection.reIndex() 或重启实例后重新生成统计信息,优化器会恢复选择索引。
不要只看 winningPlan.stage。有些查询阶段显示 FETCH,表示索引扫描后还需要回表取数据。如果索引扫描返回的键数量接近文档总数,说明该索引选择性差。此时即使走了索引,性能也不会比全表扫描好多少。可以结合 totalKeysExamined 与 nReturned 的比值判断索引是否真正有效。
三、让索引重新生效的优化方法
首先解决数据类型混乱的问题。可以用 $type 查询找出字段类型不符合预期的文档,再通过聚合管道或脚本批量转换。更新时要分批处理,避免一次性锁住大量文档影响线上服务。
db.users.updateMany(
{ age: { $type: "string" } },
[{ $set: { age: { $toInt: "$age" } } }]
)
其次要调整查询写法。对于正则匹配,尽量使用带左前缀的形式。例如用 /^张/ 代替 /张/。如果业务确实需要模糊搜索,建议单独建立全文索引或使用专门的搜索组件,而不是让普通 B-tree 索引硬扛。对于 $ne 和 $nin,可以尝试改写成 $in 正向枚举,或者拆分为多次查询后在应用层合并结果。
复合索引设计要遵循 ESR 规则:Equality fields first,Sort fields next,Range fields last。等值条件放在最前面,排序字段其次,范围查询字段最后。比如订单查询经常按 status 过滤、按 createdAt 排序、按 amount 做范围筛选,可以创建:
db.orders.createIndex({ status: 1, createdAt: 1, amount: -1 })
这样 { status: "paid" } 等值查询、{ status: "paid", createdAt: { $gte: startDate } } 范围查询以及附带 createdAt 排序的查询都能较好匹配。但如果查询中 createdAt 是范围条件,同时还要按 amount 排序,这个索引的后半段就难以利用,需要根据实际查询频率重新设计索引。
紧急情况下可以用 hint 强制指定索引,快速验证性能差异。但 hint 会绕过优化器评估,长期来看不利于统计信息变化后的自动调整。建议只用于临时诊断,最终还是要通过数据清洗和查询重写让优化器自然选择正确的索引。
db.orders.find({ status: "paid" }).hint({ status: 1, createdAt: 1 })
MongoDB 索引失效通常不是单一原因造成的。遇到查询变慢时,先查看执行计划确认是否走了索引,再检查字段类型和查询写法,最后回到复合索引设计上排错。把数据类型对齐、查询条件规范化、索引顺序设计这三件事做好,大部分索引失效问题都能得到解决。
MongoDB索引失效查询性能优化索引优化修改时间:2026-10-01 14:35:43