mongodb的$or查询不自动合并单字段索引,需为每个子句单独建立覆盖其全部条件的索引;若含排序,复合索引必须包含且顺序合规;多数组字段慎用多键索引;务必用.explain("executionstats")验证索引实际使用效果。

单个 $or 查询不自动合并索引,必须手动对齐字段顺序
MongoDB 在 2.6+ 版本确实支持为 $or 的每个子句分别使用不同索引,但前提是:每个子句必须能独立命中一个**已存在且覆盖完整查询条件**的索引。它不会自动把两个单字段索引“拼”成一个复合索引来用。
常见错误是写了这样的查询却只建了单字段索引:
db.orders.find({ $or: [ { status: "pending" }, { userId: "U1001" } ] })
如果你只建了 {status: 1} 和 {userId: 1},MongoDB 会分别用这两个索引扫描,再合并结果——这比全表扫描好,但远不如一个能同时覆盖两者的索引高效(尤其当其中一个条件选择性很低时)。
- 真正高效的方案是:分析每个
$or子句中**共有的高频过滤字段**,优先为它们建复合索引 - 如果子句间无公共字段,且各自查询频次高,保留两个单字段索引也合理,但需用
.explain("executionStats")确认totalDocsExamined是否明显下降 - 避免给低选择性字段(如
status只有 3 个取值)单独建索引,它可能被优化器跳过
$or + 排序时,复合索引必须包含排序字段且顺序合规
带 .sort() 的 $or 查询最容易踩坑:MongoDB 无法用多个索引同时满足查询和排序。例如:
db.users.find({ $or: [ { city: "Beijing" }, { tag: "vip" } ] }).sort({ createdAt: -1 })
即使你有 {city: 1, createdAt: -1} 和 {tag: 1, createdAt: -1} 两个索引,MongoDB 仍可能放弃索引排序,退化为内存排序(stage: "SORT"),触发 executionStats 中的 sortKeyPattern 为空、usedDisk 为 true 等危险信号。
- 必须确保至少一个子句的索引**完整覆盖其查询条件 + 排序字段**,且排序方向匹配(升序查就别用降序索引)
- 更稳妥的做法是把排序字段放在复合索引末尾,例如
{city: 1, createdAt: -1}支持{city: "Beijing"}.sort({createdAt: -1}),但不支持{tag: "vip"}.sort({createdAt: -1}) - 若无法统一,考虑拆成两次查询应用层合并,或改用聚合管道
$facet避免排序冲突
多数组字段参与 $or 时,慎用多键索引
当 $or 条件涉及数组字段(如 tags: ["a", "b"]),MongoDB 会为每个数组元素生成索引条目(多键索引)。这时 $or 查询可能触发大量索引扫描,性能反而恶化。
典型反例:
db.posts.find({ $or: [ { tags: "news" }, { categories: "tech" } ] })
如果 tags 平均长度为 5,categories 是单值,那么 {tags: 1} 索引实际膨胀 5 倍,而 $or 扫描会遍历所有匹配的 tags 条目再去重。
- 优先评估是否真需要
$or:能否用$in替代(如{tags: {$in: ["news", "tech"]}}),它天然适配多键索引 - 若必须
$or,且数组字段选择性差,考虑冗余一个扁平化字段(如hasNewsTag: true)并为其建单字段索引 - 用
.explain()检查indexBounds是否出现超宽范围(如"tags": ["", {}]),这是多键索引失控的标志
验证 $or 是否真走索引,盯死 executionStats 三个字段
不要只看 indexUsed 是否非空——它可能只显示第一个子句用的索引,掩盖其他子句回表扫描的事实。关键要看:
-
executionTimeMillis:对比建索引前后,下降幅度是否符合预期(通常应 >50%) -
totalDocsExamined:必须显著小于集合总文档数;若接近总数,说明某个子句失效,正在全扫 -
nReturned与totalKeysExamined的比值:理想情况应接近 1:1(即每返回 1 文档只查 1 个索引键);若比值 >5,说明索引选择性差或未覆盖
执行时加 "executionStats" 参数,别用默认的 "queryPlanner":
db.orders.find({ $or: [ { status: "shipped" }, { amount: { $gt: 1000 } } ] }).explain("executionStats")
真正难的是让所有子句在真实数据分布下都保持高选择性——这往往比写对索引语句更花时间。先跑 .explain(),再决定要不要删掉那个看似合理实则拖后腿的索引。











