热数据索引优化核心是提升有效密度:需实测字段选择性(低于0.05慎入前导位)、严格遵循ESR顺序、显式对齐partialFilterExpression与查询谓词、避免冷数据脏值混入,并以explain中totalKeysExamined/nReturned≤1.2为真实密度指标。

热数据索引密度高,但不是越密越好
热数据索引密度高本身不是目标,而是结果——它反映的是索引字段值分布是否足够“稀疏”以支撑快速跳转。真正要优化的,是索引项数量与查询效率之间的比值:在保证 totalDocsExamined 尽可能低的前提下,让索引体积最小、内存驻留率最高。
常见错误是盲目给热数据加复合索引,比如对 { status: 1, created_at: -1, customer_id: 1 } 全量建索引,结果发现 customer_id 在热窗口内选择性极低(大量重复下单用户),反而拖慢写入、挤占 WiredTiger cache。
- 用
db.orders.distinct("customer_id", { created_at: { $gt: ISODate("2024-06-01") } })实测热窗口内实际 distinct 值数量,再除以热文档总数,算出真实选择性;低于 0.05 的字段慎入复合索引前导位 - 把高选择性字段(如
order_id、payment_id)放在复合索引最左,低选择性字段(如status)靠右,严格遵循 ESR(Equality, Sort, Range)顺序 - 避免在热索引中包含写频次高的字段(如
updated_at),每次更新都会触发索引项重写;改用$currentDate+ 单独 TTL 索引管理更新时间线
partialFilterExpression 的时间边界必须和查询谓词完全对齐
索引密度再高,查不到也是白搭。MongoDB 不会自动“推导”时间范围——partialFilterExpression 是硬匹配开关,只要查询里没显式带上同条件,索引直接被忽略。
典型失效场景:db.orders.find({ customer_id: "C123" }).sort({ created_at: -1 }) 永远不会命中 { created_at: { $gt: ISODate("2024-06-01") } } 的部分索引,哪怕所有匹配文档都在热窗口内。
- 所有业务查询必须显式携带热窗口过滤,例如
{ customer_id: "C123", created_at: { $gt: ISODate("2024-06-01") } } - 统一用
ISODate()字面量,别用字符串("2024-06-01")或变量拼接,否则时区/解析失败会导致索引不生效 - 聚合管道中,
$match阶段必须出现在最前,且条件字段和partialFilterExpression完全一致,否则无法利用索引加速
滚动更新热窗口时,索引密度会阶段性失真
每天推进热窗口(比如从 created_at > ISODate("2024-06-01") 切到 created_at > ISODate("2024-06-02")),新旧索引并存期间,索引密度统计(如 db.orders.stats().indexDetails 中的 size 和 count)不能直接对比——因为旧索引仍含部分有效热数据,新索引尚未被查询覆盖。
此时 explain("executionStats") 显示的 nReturned/totalKeysExamined 比值才反映真实密度效率,而非索引元数据。
- 上线新索引后,用
db.orders.find({ created_at: { $gt: ISODate("2024-06-02") } }).hint("idx_hot_v2").explain("executionStats")验证是否真正走新索引 - 监控
totalKeysExamined / nReturned,理想值应 ≤ 1.2;若长期 > 2,说明索引字段组合或顺序有问题,需重构 - 删旧索引前,确认连续 48 小时内无任何查询
hint()或日志中出现该索引名,避免误删仍在服务的索引
冷数据字段混入热索引会隐式稀释密度
即使你只对热数据建部分索引,如果索引字段本身在冷数据中存在大量 null、空字符串或默认值(如 refund_reason: ""),WiredTiger 仍会在索引 B 树中为这些值保留位置——这部分“幽灵条目”会计入索引大小,却无法被热查询利用,实质上拉低了有效密度。
这种问题在 status、payment_method 等业务状态字段上尤为明显:冷数据中大量 status: "archived",而热数据全是 "pending" 或 "shipped"。
- 建索引前先运行
db.orders.countDocuments({ created_at: { $gt: ISODate("2024-06-01") }, status: "" }),确认热窗口内无脏值 - 对可能含空值的字段,用
sparse: true配合partialFilterExpression,双重过滤无效条目 - 定期用
db.orders.aggregate([ { $match: { created_at: { $gt: ISODate("2024-06-01") } } }, { $group: { _id: "$status", count: { $sum: 1 } } } ])检查热数据内字段值分布,及时剔除异常高频值
真正难的不是建索引,而是让每个索引项都“有活干”。热窗口滚动、字段值漂移、查询谓词松动——任何一个环节脱节,密度指标就变成假象。盯住 explain 里的实际扫描数,比盯着索引大小重要十倍。











