$match必须放在聚合管道最前面,因其前置才能利用索引加速过滤,避免全量文档进入后续阶段;否则将导致性能陡降、内存溢出或bson超限错误。

为什么$match必须放在聚合管道最前面
因为不放前面,MongoDB 就没法用索引加速,后续所有阶段(比如 $group、$unwind)都在白忙活——处理几百万条根本不需要的文档。
日志、订单、用户行为这类集合动辄上亿文档,$match 晚一步,内存和耗时就可能翻倍。不是“能不能快”,而是“不前置就大概率超时或爆 document too large 错误”。
- 只有管道开头的
$match才能走索引扫描;放在$addFields或$unwind后面,MongoDB 只能全表扫 -
$match中字段必须已存在——不能匹配$set新增的字段,否则该过滤会被优化器挪到后面,失去前置意义 - 时间范围 + 状态码这种组合查询,必须建复合索引,例如
{timestamp: 1, status: 1},单字段索引效果有限
$match放错位置的典型错误现象
查“最近一小时 500 错误”,却返回空结果或超慢响应,常见原因不是逻辑写错,而是阶段顺序反了。
错误写法示例:
[{"$unwind": "$tags"}, {"$match": {"status": 500}}]——先展开数组再过滤,$tags 有 10 个值,100 万文档就变成 1000 万,再筛 500 错误,代价爆炸。
- 现象:聚合执行时间从 200ms 涨到 12s,
explain()显示nReturned是预期的 10 倍以上 - 现象:
errmsg: "BSONObj size: 16793600 (0x1000000) is invalid",说明中间结果膨胀超 16MB 限制 - 现象:加了
$text却报错"$text operator must be the first stage",这是硬性限制,不是可选项
哪些情况不能盲目前置$match
不是所有 $match 都能扔到开头。关键看它依赖的字段是不是原始文档里就有的。
比如你用 $addFields 计算了 responseTimeMs,再按这个字段筛选,那这个 $match 就必须在 $addFields 后面——MongoDB 的优化器会自动把它往前挪,但只挪到依赖字段生成之后的位置。
-
$match里用了$expr(比如{"$expr": {"$gt": [{"$size": "$items"}, 5]}}),索引基本失效,前置也没用 - 涉及
$lookup关联后的字段过滤,$match必须在$lookup后,且建议在$unwind前完成,避免关联后数据膨胀 - 全文搜索
$text必须是第一阶段,但它本身就是一个特殊形式的$match,不参与普通字段的索引优化逻辑
验证$match是否真正生效的实操方法
别靠猜,用 explain("executionStats") 看真实行为。重点盯三个字段:
db.logs.aggregate([{"$match": {"timestamp": {"$gte": ISODate("...")}, "status": 500}}], {explain: "executionStats"})
- 看
executionStats.totalDocsExamined是否接近executionStats.nReturned;如果前者是后者的 100 倍,说明没走索引 - 看
executionStats.executionStages.stage是否为IXSCAN;如果是COLLSCAN,索引压根没被选中 - 检查
executionStats.executionStages.indexName是否是你期望的复合索引名,不是_id_或其他无关索引
最容易被忽略的是:建了索引,但字段类型不一致(比如时间存成字符串),或者查询用了 $expr 绕过简单匹配——这两类情况,$match 放再前也白搭。











