聚合查询中仅$match和$project(投影已索引字段)可走覆盖索引;$lookup、$unwind、$group、$sort及投影未索引字段均导致回表;验证需满足explain中totalDocsExamined=0且totalKeysExamined≈返回数。

聚合查询能用覆盖索引,但必须严格满足条件:所有 $match 字段 + 所有 $project 字段(不含 _id)都得落在同一个复合索引里,且不能有 $lookup、$unwind 等触发文档加载的操作。
哪些聚合阶段能走覆盖索引?
只有 $match 和 $project(仅投影已索引字段)能被完全覆盖。一旦出现以下任一情况,MongoDB 就必须回表读文档:
-
$lookup:必然访问其他集合,无法覆盖 -
$unwind:需展开数组字段,原始文档不可省略 -
$group或$sort:即使字段已建索引,聚合引擎仍需加载文档做分组/排序(除非配合$sort+allowDiskUse且索引含排序字段,但也不算“覆盖”) -
$project中包含未建索引的字段(如{name: 1, email: 1},但索引只含name)
怎么验证聚合是否真的覆盖了?
用 explain("executionStats") 查看执行计划,关键看三处:
-
executionStats.totalDocsExamined必须为0 -
executionStats.totalKeysExamined应等于返回文档数(说明只查了索引) -
executionStats.executionTimeMillis明显低于等效find()查询(尤其在大集合中)
例如:
db.orders.explain("executionStats").aggregate([
{ $match: { status: "shipped", createdAt: { $gte: ISODate("2026-01-01") } } },
{ $project: { _id: 0, orderId: 1, amount: 1, status: 1 } }
])
若结果中 totalDocsExamined > 0,说明索引缺字段或顺序不对。
复合索引字段顺序怎么排才让聚合覆盖生效?
按 ESR 规则(Equality → Sort → Range),但聚合里没有显式 $sort 时,重点是 Equality 和 Range:
- 等值字段放最前(如
status: "shipped") - 范围字段放后面(如
createdAt: { $gte: ... }) -
$project字段必须全部包含,且不能有遗漏——比如投影orderId和amount,索引就得是{status: 1, createdAt: 1, orderId: 1, amount: 1} - 索引方向(
1或-1)对覆盖无影响,但影响范围查询效率;升序更通用
容易忽略的坑:_id 和嵌套字段
覆盖索引默认不包含 _id,所以只要 $project 显式设 _id: 0,它就不参与覆盖判断;但如果没写 _id: 0,MongoDB 默认返回 _id,而绝大多数索引都不含 _id 字段(除非你手动加进去),这就直接破功。
嵌套字段(如 "user.name")必须用点号完整写出建索引,且 $project 也得用同样路径,不能靠 $addFields 动态生成——后者一定触发文档加载。
另外,$expr 中的字段(如 {$match: {$expr: {$gt: ["$amount", "$threshold"]}}})无法被任何索引覆盖,因为涉及运行时计算。


















