复合索引未命中主因是字段顺序错误,必须遵循esr原则(等值、排序、范围)并满足最左前缀匹配;真实使用情况须以explain("executionstats")中stage为ixscan为准。

复合索引未命中,大概率是字段顺序错了
MongoDB的复合索引不是“字段凑一起就有效”,而是严格按定义顺序组织B-tree结构。查询条件必须覆盖索引的最左连续前缀,否则直接退化为COLLSCAN。比如索引是{"status": 1, "userId": 1, "createdAt": -1},那只有以下查询能触发IXSCAN:
{status: "pending"}{status: "pending", userId: "U123"}{status: "pending", userId: "U123", createdAt: {$gt: ISODate("...")}}
一旦跳过status,哪怕只查{userId: "U123"}或{createdAt: {$lt: ...}},索引就完全失效。
explain("executionStats") 是唯一可信的判断依据
别靠“我建了索引所以它应该用了”做判断。真实命中情况必须看执行计划里的stage字段:
- 出现
"stage" : "IXSCAN"→ 索引被使用 - 出现
"stage" : "COLLSCAN"→ 全表扫描,索引没生效 - 注意
"nReturned"和"totalDocsExamined"差距极大时,基本可断定未走索引
在Spring Boot中,用mongoTemplate.findOne(query.explain(), clazz)就能拿到完整执行计划;在shell里直接跑:db.orders.find({status:"shipped"}).explain("executionStats")。
ESR原则决定字段排列优先级
字段顺序不能只看业务直觉,得按ESR(Equal, Sort, Range)原则排:
-
Equal:等值匹配字段放最左(如
userId、tenantId),选择性越高越靠前 -
Sort:用于
sort()的字段居中,且方向必须和索引定义一致(如索引是"createdAt": -1,查询就不能sort("createdAt", 1)) -
Range:范围查询字段(
{$gt}、{$in}等)放最后,因为B-tree只能高效支持一个后缀范围
反例:{"createdAt": -1, "status": 1} —— 若常查status = "done"再按时间倒序,这个顺序会让等值字段status被卡在后面,无法发挥最左前缀优势。
@CompoundIndex声明后不等于索引已生效
Spring Data MongoDB的@CompoundIndex只是声明,是否真正创建取决于配置和启动行为:
- 确认
spring.data.mongodb.auto-index-creation=true(默认false) - 应用启动后,进MongoDB Shell执行
db.orders.getIndexes(),检查输出里是否有你定义的索引名和键 - 如果没出现,要么配置关了自动建索引,要么类上漏了
@Document或包没被ComponentScan扫到 - 手动建索引更可控:
db.orders.createIndex({"userId": 1, "status": 1}, {name: "idx_user_status"})
字段顺序错一次,整个索引就废掉;而建了但没启用,等于白忙活。这两点在生产环境最容易被忽略,也最难排查。











