mongodb聚合中$match必须置于管道最前且作用于原始字段才能利用索引,复合索引需匹配查询字段顺序,时序集合须手动建二级索引,$lookup关联字段必须有索引,explain中keycount与docsexamined差距大表明索引未有效命中。

$match 必须放在管道最前面才能用上索引
MongoDB 的索引只对聚合管道中靠前的 $match 有效,且仅限于「未被转换过的原始字段」。一旦数据经过 $group、$lookup 或 $addFields,后续的 $match 就无法利用集合上的索引了。
常见错误是把过滤逻辑写在 $group 后面,比如先分组再筛 sum > 100 的结果——这时 $match 操作的是内存中的聚合中间结果,不是底层文档,索引完全失效。
- 正确做法:所有能基于原始字段过滤的条件,一律塞进第一个
$match阶段 - 复合索引字段顺序必须匹配查询条件顺序,例如
{ userId: 1, createdAt: -1 }能加速{ userId: ObjectId("..."), createdAt: { $gt: ... } },但对{ createdAt: ..., status: ... }无效 - MongoDB 6.0 不会自动重排你写的管道顺序来“帮你用索引”,它只做有限的阶段合并优化(比如相邻
$match合并),不会把后面的$match提前
时序集合要显式建二级复合索引
MongoDB 6.0 对时序集合(time-series collection)支持二级索引和复合索引,但默认不创建。如果你按设备 ID + 时间范围查指标,只依赖内置的时间聚簇索引不够快——尤其当设备维度基数高、时间范围窄时。
示例场景:查某台设备过去 1 小时的 CPU 使用率峰值
- 原始集合定义:
db.createCollection("metrics", { timeseries: { timeField: "ts", metaField: "device" } }) - 必须手动加索引:
db.metrics.createIndex({ device: 1, ts: -1 }) - 否则即使
$match写对了,也会全量扫描该设备所有历史分块,而不是直接定位到对应时间窗口
$lookup 关联字段没索引,性能断崖式下跌
$lookup 不是“语法糖”,它是实际发起一次子查询。如果被关联集合(from)的 foreignField 没索引,MongoDB 会为每一条主表记录做一次全集合扫描。
比如订单表 orders 做 $lookup 关联用户表 users 的 _id,但 users._id 是 ObjectId 类型——虽然 MongoDB 自动在 _id 上建了唯一索引,可一旦你关联的是 users.username,而这个字段没索引,就立刻变慢。
- 检查方式:
db.users.getIndexes()看有没有覆盖foreignField的索引 - 注意:6.0 不支持在
$lookup中用表达式(如{ $toString: "$userId" })作为localField,这种写法必然导致索引失效 - 如果关联字段类型不一致(比如一边是字符串、一边是 ObjectId),即使有索引也用不上
explain() 输出里 keyCount 和 docsExamined 差距大,说明索引没起效
执行 db.collection.aggregate([...]).explain("executionStats") 后,重点看两个字段:
-
executionStats.totalKeysExamined:实际读了多少索引条目 -
executionStats.totalDocsExamined:实际读了多少文档
理想情况是两者接近甚至相等(覆盖索引),如果 docsExamined 远大于 keyCount,说明索引只用来定位,后续仍要回表读全量文档;如果 keyCount 很小但 docsExamined 很大,大概率是 $match 没落在索引字段上,或者用了不能走索引的操作符(如 $regex 开头通配、$ne、$not)。
6.0 的 explain 输出新增了 stage 字段标注每个管道阶段是否命中索引,但别只信它——得结合 totalKeysExamined 和实际耗时交叉验证。
索引本身不解决所有问题;它只加速「定位」。真正拖慢聚合的,往往是定位之后的数据膨胀、内存排序、或跨集合关联的网络开销。这些没法靠加一个索引就搞定。











