索引被使用但totalDocsExamined远大于nReturned,本质是索引未覆盖查询导致大量FETCH;复合索引字段顺序错误(范围在等值前)、$ne/$exists语义限制、分片下唯一索引未以片键为前缀、类型不匹配等均会导致此问题。

索引确实被用了,但 totalDocsExamined 远大于 nReturned —— 这是“走索引还慢”的核心信号,本质是索引没覆盖查询,导致大量白读(FETCH)。
为什么IXSCAN之后还要FETCH上百万文档
IXSCAN只扫描索引项,但若查询条件或投影字段不在索引里,MongoDB必须把每个匹配的索引条目对应的完整文档从磁盘/内存中读出来(FETCH),再逐个做字段过滤或投影。这一步开销极大,尤其当 totalDocsExamined 是 nReturned 的几十倍甚至上千倍时。
- 典型表现:
explain("executionStats")返回中totalDocsExamined: 2480000,nReturned: 12 - 常见诱因:查询含
$exists、$ne、$not,或复合索引字段顺序错误(比如范围字段写在等值字段左边) - 验证方式:盯住
executionStages.stage是否为"IXSCAN"且docsExamined === nReturned;不相等,就说明没真正覆盖
复合索引字段顺序错:等值必须在前,范围必须在后
MongoDB 复合索引遵循严格前缀匹配。顺序错了,索引就退化成范围扫描,后续字段全部失效。
- 错误示例:查询
{"status": "active", "created_at": {"$gte": ISODate("2025-01-01")}},却建了{"created_at": 1, "status": 1}—— status 完全无法跳转,只能扫时间范围再逐个过滤 - 正确做法:建
{"status": 1, "created_at": 1},让 status 精准定位起始位置,再按时间范围向右延伸 - 注意:
$in算“伪等值”,可放中间,但别放最后;$exists基本不参与前缀匹配,除非配合partialFilterExpression
$ne 或 $exists 查询慢不是配置问题,是语义限制
$ne 在 B-tree 上天然难优化:它等价于两个不连续范围,且默认匹配字段缺失/为 null/为 undefined 的文档,优化器通常放弃智能拆解。
- 现象:
totalKeysExamined接近索引总项数,totalDocsExamined接近集合总文档数 - 替代方案优先级:用
$in白名单 > 建部分索引(partialFilterExpression: {status: {$in: ["active", "pending"]}}) > 覆盖索引({status: 1, _id: 1, name: 1}+ 投影) - 关键约束:部分索引只对显式命中
partialFilterExpression的查询生效;{$or: [{status: "active"}, {status: "pending"}]}不触发
分片集群下唯一索引没报错?片键前缀漏了
在分片集群中,createIndex({"email": 1}, {unique: true}) 能成功,但跨分片重复插入不会报错——因为唯一性只在单分片内校验。
- 必须先查片键:
sh.status(),确认片键如{"orgId": 1} - 唯一索引 key 必须以片键开头:
{"orgId": 1, "email": 1},否则无效 - 重建前删旧索引:
db.users.dropIndex({"email": 1}),再建带前缀的,并加background: true - 额外提醒:如果业务还需按
email单独查,得额外建一个非唯一索引{"email": 1},否则这个查询会退化为广播查询
最常被忽略的一点:类型和路径必须完全一致。字段存的是 "active"(string),Go 驱动传 int64(1) 或 nil,索引根本不会命中——不是慢,是压根不用。











