MongoDB查询未走索引会触发COLLSCAN导致性能骤降,应通过db.collection.explain("executionStats").find()检查executionStage是否为"IXSCAN",并规避函数运算、非前缀正则、复合索引顺序错位等索引失效场景。

db.collection.find() 执行时没走索引,就会触发 COLLSCAN —— 这是百万级数据下查询慢的头号原因。根本解法不是调优语句,而是让查询「必须走索引」。
怎么判断当前查询是否在全表扫描?
执行 db.collection.explain("executionStats").find({ ... }),看返回里的 executionStage 字段:如果值是 "COLLSCAN",说明没走索引;如果是 "IXSCAN",才表示用了索引。
注意:explain() 默认只显示查询计划,加 "executionStats" 才能看到真实执行路径和扫描文档数。不加这个参数,容易误判索引是否生效。
- 别只看
queryPlanner.winningPlan,它只告诉你“选了哪个计划”,不反映实际执行情况 - 聚合管道中每个
$match阶段都要单独explain,不能只查最外层 -
db.collection.getIndexes()查不到索引?可能你连错库或集合名拼错了,先确认use your_db和db.your_collection.find().limit(1)能正常返回
哪些写法会让索引失效?
常见陷阱不是“没建索引”,而是“建了但用不上”。MongoDB 对索引使用很严格,以下操作会直接退化为 COLLSCAN:
- 对字段做函数运算:
db.users.find({ $expr: { $gt: { $year: "$birthday" }, 1990 } })——$year拆解日期,无法命中{ birthday: 1 }索引 - 使用
$ne、$not、$regex(非前缀):db.users.find({ name: { $regex: "son$" } })无法用普通 B-tree 索引 - 复合索引顺序错位:
db.orders.createIndex({ status: 1, createdAt: 1 }),但查询只用{ createdAt: { $gt: ISODate(...) } }—— 缺少前置字段status,索引被跳过 - 类型不匹配:
db.logs.find({ level: "ERROR" }),但level字段实际存的是数字500,BSON 类型不一致导致索引失效
更新/删除操作也得防 COLLSCAN
db.collection.updateOne() 和 db.collection.deleteMany() 的过滤条件同样受索引约束。没索引的 update 不仅慢,还会长时间持有写锁,阻塞其他操作。
- 哪怕只改一个字段,
updateOne({ _id: ObjectId("...") }, { $set: { status: "done" } })也依赖_id索引 —— 幸好_id默认有唯一索引,但自定义字段就得自己建 -
deleteMany({ ts: { $lt: ... } })必须给ts建索引,否则删几万条可能卡住整个副本集的写入队列 - 批量操作前先
explain("executionStats"),别等线上跑了才发现在扫全表
索引不是越多越好,但关键字段必须覆盖
索引要权衡写入开销和查询收益。真正该建索引的,只有那些高频出现在 find、update、delete 条件里的字段,尤其是组合条件中的前导字段。
- 优先建单字段索引:
db.users.createIndex({ email: 1 }),比盲目建复合索引更稳妥 - 复合索引按“等值 → 范围 → 排序”排序:
db.events.createIndex({ type: 1, createdAt: -1, userId: 1 })支持{ type: "login" }+{ createdAt: { $gt: ... } }+.sort({ userId: 1 }) - 定期用
db.collection.stats().indexCount和db.collection.totalSize()看索引膨胀比,超过 30% 就该考虑重建或精简
explain("executionStats") 输出来验证。线上跑着的 updateMany 或 deleteMany,只要没显式指定索引字段,就默认裸奔——这点最容易被忽略。

















