必须按esr规则创建复合索引{status: 1, userid: 1, createdat: -1},即等值字段(status、userid)在前,排序兼范围字段(createdat)在后,才能同时支撑状态筛选、用户id过滤和时间范围排序查询。

要在MongoDB中让一个复合索引同时支撑按状态筛选、按用户ID过滤、再按时间范围排序的查询,必须严格遵循字段顺序与查询模式的匹配逻辑,否则索引将完全失效。
理解最左前缀原则是设计前提
复合索引本质上是B-Tree结构,数据按索引字段从左到右逐级排序。只有查询条件包含索引最左侧连续字段时,MongoDB才会使用该索引。例如索引 {a: 1, b: 1, c: 1} 可命中 {a: 1}、{a: 1, b: 1}、{a: 1, b: 1, c: 1},但 【绝不会命中 {b: 1} 或 {c: 1} 单独出现的查询】。
这一步不能跳过——直接建索引却不验证查询是否命中,等于白建。
按ESR规则排列字段顺序
ESR即 Equal → Sort → Range,是复合索引字段排序的黄金法则:
- 第一步:把所有等值查询(=、in)字段放最左,且高频字段优先;
- 第二步:紧接其后放排序字段(sort),仅支持一种方向(升/降序);
- 第三步:最后放范围查询字段($gt、$lt、$regex 等),且只能有一个范围字段在末尾。
若违反此序,比如把范围字段放在中间,MongoDB会在该字段之后截断索引使用,后续字段全部失效。
实战创建支持三类查询的复合索引
假设业务中存在以下高频查询:
db.orders.find({status: "paid", userId: "u123"}).sort({createdAt: -1})db.orders.find({status: "paid"}).sort({createdAt: -1})db.orders.find({status: "paid", userId: "u123", createdAt: {$gte: ISODate("2026-01-01")}})
对应索引应为:db.orders.createIndex({status: 1, userId: 1, createdAt: -1})。
这里 status 是等值字段→userId 是等值字段→createdAt 是排序+范围双重用途字段,符合 ESR 规则。注意:createdAt 同时承担 sort 和 range 角色是允许的,但必须放在末尾。
验证索引是否真正生效
方法一:用 explain() 查看执行计划
执行 db.orders.find({status: "paid", userId: "u123"}).sort({createdAt: -1}).explain("executionStats"),检查返回中的 executionStages.stage 是否为 IXSCAN,且 nReturned 接近 totalDocsExamined 值——说明几乎没扫描无关文档。
方法二:对比无索引时的性能落差
临时删除索引 db.orders.dropIndex("status_1_userId_1_createdAt_-1"),重跑相同查询,观察 executionTimeMillis 是否暴涨 5 倍以上。暴涨即证明原索引确实在起效。
避免踩坑的两个关键动作
动作一:禁用重复单字段索引
如果已存在 {status: 1} 和 {userId: 1} 单字段索引,务必删除它们。复合索引 {status: 1, userId: 1, createdAt: -1} 已能覆盖这两个单字段查询,多留反而拖慢写入速度。
动作二:控制字段总数不超过32个
MongoDB硬性限制单个复合索引最多含32个字段。若业务真需更多维度,应拆分为多个专用复合索引,而非堆砌字段。例如把地理位置相关字段单独建 {location: "2dsphere"} 索引。











