低基数字段单独建索引基本没用,因索引扫描成本接近全表扫描且加剧写放大;可用部分索引只索引高区分度子集,并确保查询严格匹配过滤条件才能生效。

为什么低基数字段单独建索引基本没用
像 gender、is_deleted、status(只有 "active"/"inactive" 两种值)这类字段,MongoDB 即使建了索引,查询时也大概率走 COLLSCAN。因为优化器发现:扫描 95% 的索引键,和直接扫全表文档,成本差不多——索引反而多了一层跳转开销。
- 执行
db.users.find({ gender: "female" }).explain("executionStats"),观察totalKeysExamined与totalDocsExamined是否接近 - 若两者比值 > 0.8,说明索引未被有效利用
- 低基数字段 + 高频更新(如频繁 toggle
is_deleted)还会加剧写放大,拖慢整体吞吐
用部分索引(partial index)绕过低基数陷阱
不给所有值建索引,只对“有区分度”的子集建——比如只索引 status: "active" 的用户,而忽略 "inactive" 的百万条沉睡数据。
- 创建方式:
db.users.createIndex({ email: 1 }, { partialFilterExpression: { status: "active" } }) - 该索引仅包含
status === "active"的文档,体积小、内存友好、命中率高 - 查询必须带完整过滤条件才能命中:
db.users.find({ status: "active", email: "a@b.com" })—— 缺少status: "active"就不会用这个索引 - 注意:
partialFilterExpression中不能用$regex、$where等非确定性表达式
复合索引里混入低基数字段怎么救
如果业务强制要查 { status: "active", createdAt: { $gt: ... } },但 status 基数低,别把它放最左——把高区分度字段前置,再用 partialFilterExpression 收口。
- 错误示范:
db.orders.createIndex({ status: 1, createdAt: -1 })→status在左,范围查询一加,后面字段全失效 - 正确做法:
db.orders.createIndex({ createdAt: -1 }, { partialFilterExpression: { status: "paid" } }) - 或更稳妥的复合部分索引:
db.orders.createIndex({ status: 1, createdAt: -1 }, { partialFilterExpression: { status: { $in: ["paid", "shipped"] } } }) - 验证是否生效:用
.explain("queryPlanner")检查indexName和indexBounds
部分索引上线前必须检查的三件事
部分索引不像普通索引“建了就用”,它对查询写法极其敏感,漏掉一个条件就退回全表扫。
- 确认应用层所有相关查询都显式带上
partialFilterExpression中的字段和值(大小写、类型、空格都要一致) - 用
db.collection.getIndexes()查看索引定义中是否有partialFilterExpression字段,避免误建为全量索引 - 在测试环境用真实数据跑压测,对比
executionTimeMillis和totalDocsExamined—— 上线后一旦发现慢查,优先怀疑是不是某处漏传了status
.find({ email: x }),根本没意识到自己已经脱离了部分索引的覆盖范围。


















