MongoDB 7.0中$match必须前置,因仅管道开头的$match能走索引;复合索引须严格遵循ESR顺序(等值→排序→范围),缺失最左等值字段则索引完全失效。

复合索引在 MongoDB 7.0 的聚合管道中只对前置的 $match 有效,且字段顺序必须严格匹配查询条件中的等值字段优先原则 —— 这是提速最关键的硬约束。
为什么 $match 必须放在聚合管道最前面
MongoDB 7.0 的查询优化器不会把靠后的 $match “上提”到能用索引的位置。哪怕你写的是 {$lookup}, {$match: {status: "paid"}},只要 $match 在 $lookup 后面,它就只能扫内存里的中间结果,无法触发集合索引。
- 只有位于管道开头、直接作用于原始集合文档的
$match阶段,才可能命中索引 - 如果
$match出现在$group或$sort后,它处理的是已聚合/已排序的临时数据,索引完全失效 - 使用
explain: "executionStats"可验证:看executionStages.stage是否出现IXSCAN,且nReturned明显小于nDocumentsExamined
复合索引字段顺序必须遵循 ESR 规则
ESR(Equality → Sort → Range)不是建议,而是 MongoDB 7.0 索引扫描路径的执行逻辑。比如你要查 {status: "shipped", order_date: {$gte: ISODate("2025-01-01")}, region: "US"},索引必须定义为 db.orders.createIndex({status: 1, region: 1, order_date: 1}),而不是 {order_date: 1, status: 1}。
- 等值字段(
status,region)必须放最前,且顺序可交换,但必须连续 - 排序字段(如
order_date: -1)只能跟在等值字段后,且只能有一个方向(升序或降序)参与索引扫描 - 范围字段(
$gte,$lt)必须放在最后;一旦出现,后续字段索引失效 - 错误示例:
db.orders.createIndex({order_date: 1, status: 1})对{status: "shipped", order_date: {$gte: ...}}仅能利用status,order_date范围部分不走索引
$lookup 关联性能卡点在哪
$lookup 本身不走主集合索引,但它依赖被关联集合(from)上的索引。如果没建对,就会退化成 collection scan,哪怕主管道有完美索引也白搭。
- 被关联集合必须在
foreignField上有单字段索引,或与localField组合的复合索引 - 例如
{$lookup: {from: "users", localField: "user_id", foreignField: "id", as: "user_info"}},必须在users集合上建db.users.createIndex({id: 1}) - 如果
$lookup前没做$match过滤,它会尝试关联全部输入文档 —— 即使最终只保留 10 条,也可能触发数万次远程查询 - 7.0 支持
let+pipeline形式子查询,此时子 pipeline 内部的$match若能命中索引,也能显著减压
$project 和字段精简的实际效果有限
MongoDB 7.0 会自动优化字段投影,把 $project 提前或合并,并不依赖你手动写在管道开头。真正影响性能的是“是否让 $match 尽早过滤掉大部分文档”,而不是“是否提前去掉几个字段”。
- 不要为了“省流量”在管道早期加
{$project: {user_id: 1, amount: 1, _id: 0}}—— 优化器已经做了 - 但如果你的
$group只需要user_id和amount,而原始文档带 50 个字段,那在$match后紧跟$project仍有助于减少内存占用(尤其配合$group这种阻塞阶段) - 重点永远在:先用索引砍掉 90% 文档,再考虑字段瘦身;反过来做,等于给大象称体重前先剪指甲
最常被忽略的一点:复合索引一旦创建,$match 中缺失最左字段(比如只查 order_date 而不带 status),整个索引就不可用 —— 不是“部分生效”,是彻底跳过。这和 SQL 的最左前缀原则一致,但在聚合场景里更容易因管道结构遮蔽而误判。

















