部分索引通过partialFilterExpression仅对高频查询的文档子集建索引,减少索引条目数,从而显著降低磁盘占用、写入开销和内存压力;其大小正比于条目数,剔除90%条目可减空间85%+。

为什么部分索引能减少磁盘占用?
MongoDB 的索引是为集合中**每一条匹配条件的文档**单独建立条目的。如果集合里有 1 亿条日志,其中 95% 是半年前的冷数据(几乎不查询),但你建了一个全字段 {status: 1, created_at: -1} 的复合索引,那这 95% 的旧文档依然会贡献索引条目——它们占空间、拖慢写入、增加内存压力。部分索引通过 partialFilterExpression 显式排除这些低频数据,让索引只覆盖真正高频查询的子集。
关键点在于:索引大小 ≈ 索引条目数 × 单条索引记录平均体积。删掉 90% 的条目,磁盘占用通常能下降 85%+(实测常见于时间分区类场景)。
怎么定义“低频数据”的过滤条件?
不能凭感觉写 partialFilterExpression。必须和真实查询模式对齐,否则索引会被跳过:
- 查最近 7 天订单?用
{created_at: {$gt: ISODate("2024-06-01T00:00:00Z")}}
- 只查已支付订单?用
{status: "paid"}(前提是 status 字段高基数且查询集中)
- 混合条件更稳妥:比如
{status: "paid", created_at: {$gt: ...}},确保所有高频查询都命中该表达式
{created_at: {$gt: ISODate("2024-06-01T00:00:00Z")}}
{status: "paid"}(前提是 status 字段高基数且查询集中){status: "paid", created_at: {$gt: ...}},确保所有高频查询都命中该表达式⚠️ 容易踩的坑:partialFilterExpression 中不能用 $or、$regex、$text;也不能引用不存在的字段(会导致索引创建失败);时间字段必须是 BSON Date 类型,字符串格式的日期(如 "2024-06-01")无法被正确比较。
创建部分索引时的关键参数和验证方法
直接运行 createIndex 命令即可,但必须显式指定 partialFilterExpression:
db.orders.createIndex(
{ user_id: 1, created_at: -1 },
{
partialFilterExpression: {
status: "paid",
created_at: { $gt: ISODate("2024-06-01T00:00:00Z") }
},
name: "idx_user_paid_recent"
}
)
创建后立刻验证是否生效:
- 查索引大小:
db.orders.stats().indexDetails["idx_user_paid_recent"].size(单位字节) - 确认是否被查询使用:
db.orders.find({user_id: 123, status: "paid", created_at: {$gt: ...}}).explain("executionStats"),看executionStats.nReturned和executionStats.totalKeysExamined是否合理(后者应明显小于全量索引) - 故意查一个被过滤掉的文档(如旧的
status: "pending"),确认执行计划里显示IXSCAN未被选用(而是走COLLSCAN或其他索引)
部分索引上线后的维护注意事项
它不是一劳永逸的“减肥药”。随着业务演进,过滤边界会漂移:
- 时间阈值要定期更新:每月初用
collMod 动态调整 partialFilterExpression(MongoDB 4.2+ 支持),或重建索引
- 避免过度细分:不要为每个自然月建一个部分索引。3–6 个月滚动窗口 + TTL 配合更可控
- 写放大风险:如果新插入的文档大量落在过滤范围外(比如突然导入历史数据),它们不进索引,但查询时可能触发全表扫描——监控
collectionScan 指标很关键
collMod 动态调整 partialFilterExpression(MongoDB 4.2+ 支持),或重建索引collectionScan 指标很关键最常被忽略的一点:部分索引不会自动清理已落出过滤范围的旧索引条目。MongoDB 不支持“收缩”已有索引,只能重建。所以首次上线前,最好先用 db.orders.count() 和带相同条件的 countDocuments() 估算实际索引覆盖率,再决定是否值得重构。










