$match应尽量前置以利用索引、减少数据量,但仅适用于原始存在且已建索引的字段;依赖$addfields或$lookup生成的字段需在其后使用$match。

为什么$match必须放在聚合管道最前面
因为MongoDB只对「第一个能走索引的$match」生效,后面的$match基本无法利用索引——数据已经过$unwind、$lookup或$addFields变形,原始字段结构和索引不匹配了。比如你先$unwind再$match数组元素,那这个$match只能全表扫。
常见错误现象:explain("executionStats")里totalDocsExamined远大于nReturned(比如 200 万 vs 150),说明大量文档被后续阶段白白搬运;或者stage: "COLLSCAN"出现在第一个$match下,直接暴露索引没命中。
- 复合索引字段顺序必须和
$match中等值查询字段顺序一致:索引{userId: 1, createdAt: 1}能加速{$match: {userId: "u1", createdAt: {$gte: ...}}},但对{$match: {createdAt: ..., userId: "u1"}}可能完全失效 - 类型必须严格一致:索引建在
ObjectId上,查询却传字符串"507f1f77bcf86cd799439011",索引跳过 - 别在
$match里混用$or和非索引字段,整个条件可能放弃索引走全表扫描
哪些$match不能盲目前置
不是所有$match都能往开头塞。如果它依赖前面阶段生成的字段,比如$addFields加的isVIP、$lookup关联后的orderCount,就必须放在对应阶段之后,否则查不到数据。
使用场景举例:你要统计“近30天下单超过5次的VIP用户”,$match里要同时用原始字段createdAt和计算字段orderCount,那就得拆成两个$match——第一个放最前过滤时间,第二个放$group和$addFields之后再筛次数。
- 前置
$match只适用于原始集合中已存在、且已建索引的字段 -
$lookup内部的pipeline里也有$match,那个$match的索引必须单独建在from集合上,和主集合无关 - 用
$expr的$match(比如{$match: {$expr: {$gt: ["$a", "$b"]}}})基本不走索引,别指望它提速
怎么验证$match真的走了索引
跑explain("executionStats")是最直接的方式。重点盯三个值:
totalDocsExamined ≈ totalKeysExamined 且远小于集合总文档数 → 索引生效;totalDocsExamined ≈ 集合总文档数 → 全表扫描;totalKeysExamined明显小于totalDocsExamined → 索引部分生效但没覆盖全部查询条件。
- 看到
stage: "IXSCAN"说明走索引了;出现"COLLSCAN"就得立刻检查索引是否存在、字段顺序是否匹配 -
executionStages.nReturned如果远小于executionStages.totalDocsExamined,说明过滤太晚,该往前挪$match - 注意
$lookup阶段下的from集合也要单独跑explain,它的COLLSCAN和主集合索引无关
allowDiskUse和$project怎么配合$match一起用
$match前置只是第一步。如果过滤后数据量依然大,后面跟$group或$sort,内存很容易超100MB默认限制。这时候allowDiskUse: true不是“可选开关”,而是“必开配置”——否则会直接报错"Exceeded memory limit"。
但开了不等于万事大吉。$project必须紧接在$match之后,只保留$group真正需要的字段(比如只留userId和amount),否则中间文档体积过大,磁盘I/O反而拖慢整体速度。
- 不要写
{$project: {_id: 0}}这种默认全字段投影,显式列出要用的字段 - 对嵌套大字段(日志内容、base64图片)务必在
$match后立刻$project剔除 - 生产环境聚合命令建议始终带
{allowDiskUse: true},避免静默失败,但必须配合explain确认是否真触发落盘
$unwind或$lookup后文档数量的爆炸式增长——这时$match再前置也救不了,必须靠$project提前精简字段,或者用$limit控制输入规模。











