$match必须置于聚合管道最前端才能利用索引;后置$match因文档结构已变导致索引失效,等值字段须在复合索引最左,范围查询仅能跟在其后且限一个。

$match 必须放在管道最前面,否则几乎一定慢。MongoDB 只有在聚合管道第一个阶段命中索引的 $match 才能避免全表扫描;后续阶段的 $match 无法利用索引,数据已变形或聚合过,索引失效。
为什么后置 $match 会彻底失去索引能力
MongoDB 的索引只能作用于原始文档结构。一旦管道经过 $group、$lookup、$unwind 或任意字段计算(如 $addFields),输出文档的字段路径、类型甚至存在性都可能改变,索引无法匹配。
常见错误现象:
- 执行
db.collection.aggregate([{$group: {...}}, {$match: {status: "done"}}])时,executionStats.totalDocsExamined等于集合总文档数 -
$explain显示stage: "COLLSCAN",而非"IXSCAN" - 即使
status字段有单列索引,也完全不生效
复合索引必须严格匹配 $match 前置字段顺序
索引不是“有就行”,它必须覆盖前置 $match 中所有等值查询字段,并按顺序排列,才能被真正使用。
例如这个查询:
db.orders.aggregate([
{$match: {userId: ObjectId("..."), orderDate: {$gte: ISODate("2023-01-01")}}}
])
需要的索引是:
db.orders.createIndex({userId: 1, orderDate: 1})
而不是 {orderDate: 1, userId: 1} —— 后者无法支持 userId 等值 + orderDate 范围查询的高效定位。
ZenHeart zenlink 与 zenlink-mcp 为 OpenClaw,涵盖安装、MCP 注册、钩子接线、入站轮询及升级维护。
关键细节:
- 等值字段(
userId: ...)必须放在复合索引最左侧 - 范围字段(
orderDate: {$gte: ...})只能跟在等值字段之后,且仅能有一个用于范围扫描 -
$sort若紧接在前置$match后,可复用同一索引完成排序(如{userId: 1, orderDate: -1}支持{$match: {userId: ...}}, {$sort: {orderDate: -1}})
Node.js 驱动中容易被忽略的陷阱
在 Express 或 Koa 的路由处理中,开发者常把业务逻辑判断塞进聚合管道后期,误以为“先算再筛”更自然,结果性能断崖下跌。
典型反模式:
db.orders.aggregate([
{$lookup: {from: "users", localField: "userId", foreignField: "_id", as: "user"}},
{$unwind: "$user"},
{$match: {"user.role": "premium"}} // ❌ 这里无法走索引!
])
正确做法是把筛选条件前移到关联前:
db.orders.aggregate([
{$match: {userId: {$in: premiumUserIds}}}, // ✅ 利用 userId 索引快速缩小主集合
{$lookup: {from: "users", localField: "userId", foreignField: "_id", as: "user"}},
{$unwind: "$user"}
])
其他易踩点:
- 用
new Date().toISOString()拼字符串传给$match,导致日期类型不一致,索引失效 - 在
$match中混用$expr(如{$expr: {$gt: ["$a", "$b"]}}),这类表达式永远无法使用索引 - 忘记对
allowDiskUse: true做兜底,当$group数据量大时直接报错退出,而不是降级到磁盘缓存
真正卡住性能的,往往不是聚合逻辑本身,而是第一道 $match 没拦住数据。只要它没放对位置、没配对索引,后面所有优化都是徒劳。

















