90%的“有索引还慢”问题源于totaldocsexamined远大于nreturned,主因是索引未覆盖查询导致大量fetch白读;常见诱因包括非前缀操作符、复合索引字段顺序错误、多键索引膨胀、类型/路径不匹配、分片唯一索引缺片键前缀及gridfs缺关键索引。

explain("executionStats")里totalDocsExamined远大于nReturned
这是90%的“有索引还慢”问题的根源。索引确实被用了(IXSCAN阶段存在),但MongoDB扫了一大片索引范围后,还得把每个匹配的文档从磁盘或内存里完整读出来(FETCH),再逐个做字段过滤——totalDocsExamined就是这个“白读”的数量。
常见诱因:
- 查询含
$exists、$ne、$not等非前缀操作符,索引无法跳过无关文档 - 复合索引字段顺序错:范围查询字段(如
{"created_at": {"$gte": ...}})放在了等值字段(如"status": "active")左边 - 数组字段(如
producttags)触发多键索引,导致totalKeysExamined爆炸式增长
验证方式:db.collection.find({...}).explain("executionStats"),盯死totalDocsExamined和nReturned的比值。比值>10就该优化;>100基本等于索引没覆盖。
字符串字段类型不匹配或路径不对
MongoDB对BSON类型极度敏感。字段存的是"active"(string),但Go驱动里传了int64(1)或nil,索引完全不命中——不是慢,是根本不用。
排查步骤:
- 用
db.collection.findOne()看真实存储值和类型,确认是string还是int或boolean - 建索引时用
bson.D{{"status", 1}}没问题,但查询条件必须严格一致:bson.M{"status": "active"},不能是bson.M{"status": 1} - 字段嵌套在
metadata里?索引必须建在"metadata.projectId",而不是"projectId"
MySQL同理:WHERE name = ?传""或nil会让优化器弃用索引;UPPER(name) = 'ABC'也一样——函数包裹直接绕过B-tree。
分片集群中唯一索引未带片键前缀
在分片集群里,createIndex({"email": 1}, {unique: true})能成功,但插入重复email不会报错。因为索引只在单个分片本地生效,跨分片无法校验唯一性。
合规做法:
- 先查片键:
sh.status() - 唯一索引key必须以片键字段开头,例如片键是
{"orgId": 1},则唯一索引只能是{"orgId": 1, "email": 1} - 删掉旧索引:
db.users.dropIndex({"email": 1}) - 重建带前缀的:
db.users.createIndex({"orgId": 1, "email": 1}, {unique: true, background: true})
注意:如果业务还要按email单独查,得额外建一个非唯一索引{"email": 1},否则这个单字段查询会变慢——复合索引的前缀部分无法被单独利用。
GridFS元数据或chunks缺关键索引
fs.files默认只有_id索引,按filename查就是全表扫描;fs.chunks缺{files_id: 1, n: 1}索引,下载时会暴力扫所有chunk。
紧急补救:
-
db.fs.files.createIndex({"filename": 1, "uploadDate": -1})(高频按名查+时间排序) -
db.fs.chunks.createIndex({"files_id": 1, "n": 1}, {unique: true})(unique防孤儿chunk) - 避免在
metadata里用$regex或$in查文件,它们几乎不走索引
驱动层别依赖findOne()自动重试——网络抖动时可能查不到还留孤儿chunk。显式加maxTimeMS和readPreference: "primary"更可控。
explain("executionStats")里那三个数字的差值,以及类型、路径、分片规则这些容易被忽略的硬约束。











