必须对fs.files建{filename:1,uploadDate:1}复合索引,因GridFS默认索引仅首次写入时自动创建且极易缺失,缺失时百万级文件查询将退化为全表扫描;需用getIndexes()确认存在、background:true安全创建、并严格按索引顺序查询才能生效。

必须对 fs.files 集合建复合索引,仅靠默认索引完全不够用——数百万小文件下,没索引的 find({ filename: "x" }) 就是全表扫描,响应从毫秒拖到秒级甚至超时。
为什么 { filename: 1, uploadDate: 1 } 复合索引是刚需
GridFS 默认会在首次写入时自动创建 { filename: 1, uploadDate: 1 } 索引,但这个“默认”极不可靠:集合已存在、手动清空过 fs.files、或用旧驱动初始化,都可能导致该索引根本不存在。你查 db.fs.files.getIndexes(),如果输出里没有 { "key": { "filename": 1, "uploadDate": 1 } },那就等于裸奔。
常见错误现象:
- 只建了
{ filename: 1 }单字段索引,但查询带uploadDate: { $gte: ... }时照样走不了索引 - 索引存在但
size字段远小于文档总数(比如 100 万文档,索引 size 才 2 万),说明索引损坏或未完成构建
建索引不能直接 createIndex,得避开三重风险
对数百万文档建索引会阻塞写入、吃光内存、压垮磁盘 I/O,尤其在机械硬盘上更明显。别信“后台建就没事”——它只是不锁表,但 CPU 和磁盘负载照常飙升。
- 必须加
{ background: true }:例如db.fs.files.createIndex({ filename: 1, uploadDate: 1 }, { background: true }) - 绝对避开业务高峰:哪怕后台建,也建议在凌晨或低峰期执行
- 监控是否卡住:用
db.currentOp({ "secs_running": { $gt: 60 } })查长时间运行操作 - 禁用
ensureIndex:该方法已弃用,行为不稳定,MongoDB 官方明确不推荐
查询写法错,索引就是摆设
索引建好了,不代表查询就快。GridFS 的封装层(如 GridFS.find())在某些驱动版本中会额外加过滤条件,导致绕过索引;更常见的是查询条件没对齐索引顺序。
- 必须用
db.fs.files.find()直接查,别依赖高层封装 - 条件顺序要严格匹配索引:
{ filename: "a", uploadDate: { $gte: ISODate("...") } }能命中;但只查{ uploadDate: ... }或{ filename: /pattern/ }就无法利用 - 如果高频按时间范围查(如最近 7 天),单独为
uploadDate建个单字段索引更高效:db.fs.files.createIndex({ uploadDate: -1 }) - 对
metadata字段查业务属性(如project、tags),必须上传时就传入,并显式建索引:db.fs.files.createIndex({ "metadata.project": 1 })
真正容易被忽略的点:索引不是一劳永逸
频繁增删小文件会让 B-tree 索引页分裂、统计信息陈旧,explain("executionStats") 里出现 totalDocsExamined 远大于 nReturned 就是信号。这时光重建索引还不够——reIndex 只优化树结构,compact 才回收物理空间,但后者要求停写且耗资源。更可持续的做法,是重新评估是否真该用 GridFS 存小文件:平均大小低于 1MB 的,优先考虑 BinData 字段 + 元数据索引,或直接切到对象存储(S3/OSS)+ MongoDB 存元数据。


















