不适合——低基数字段(如status仅active/inactive)建B+树索引会导致空间浪费、写入变慢、优化器弃用;因其重复值多,索引过滤效果差,查询仍需大量回表,实际性能不优于全表扫描。

低基数字段为什么不适合建索引
当字段值重复率极高(比如 status 只有 "active"、"inactive" 两种),MongoDB 的 B-tree 索引会退化成“长链表”结构:大量文档指向同一索引节点,查询时仍需遍历大量键值对。此时 explain() 中常出现高 keysExamined 与低 docsReturned 的比值,说明索引没真正过滤数据。
更关键的是写入开销——每个插入/更新都要维护该索引,而它几乎不加速查询。实测显示,对基数
替代方案:部分索引 + 查询重写
如果必须按低基数字段过滤(如只查 status: "archived" 的冷数据),优先用 sparse 或 partial 索引缩小索引体积:
-
db.orders.createIndex({ archived_at: 1 }, { partialFilterExpression: { status: "archived" } })—— 只索引满足条件的文档,索引大小可降 90%+ - 配合查询强制使用该索引:
db.orders.find({ status: "archived", archived_at: { $gt: ISODate("2025-01-01") } }).hint({ archived_at: 1 }) - 避免在
$or或$in中混用低基数字段,否则优化器大概率放弃索引
复合索引中低基数字段的位置陷阱
把低基数字段放在复合索引开头(如 { status: 1, created_at: -1 })基本等于没建索引——因为前缀匹配失效,MongoDB 无法跳过大量重复 status 值直接定位 created_at 范围。
正确做法是遵循 ESR 规则(Equal → Sort → Range):
- 等值查询字段放最前,但仅限**高选择性字段**(如
user_id) - 低基数字段应放在等值段之后,且仅当它参与排序或范围查询时才有效(如
{ user_id: 1, status: 1, created_at: -1 }) - 若查询只含
status和created_at,宁可建{ created_at: -1, status: 1 }并接受全索引扫描,也别让status当前缀
真正有效的兜底手段:TTL 索引 + 数据分层
对长期存在低基数字段的场景(如日志状态、订单状态),与其硬扛索引低效,不如从数据生命周期入手:
- 用 TTL 索引自动归档冷数据:
db.logs.createIndex({ status: 1, expires_at: 1 }, { expireAfterSeconds: 0 }),再通过$set更新expires_at控制保留策略 - 将活跃态(
"pending","processing")和终态("completed","failed")拆到不同集合,用聚合管道$lookup按需关联 - 对终态数据启用压缩存储(如
zstd),降低docsExamined的物理 IO 成本
低基数不是技术问题,而是数据建模信号——它往往意味着你该重新思考状态流转是否该用文档嵌套、事件溯源或物化视图来替代简单字段枚举。



















