$match必须前置$unwind才能避免数据爆炸和索引失效,因$unwind会将含数组文档复制展开,导致中间结果量激增、内存溢出且无法利用索引;正确做法是先用$match过滤原始字段并确保对应索引存在。

$match 放在 $unwind 后面会导致数据爆炸式膨胀,索引完全失效
为什么 $unwind 后过滤会拖垮性能
因为 $unwind 会把一个含数组的文档展开成多份——原始 1 条文档,若数组有 5 个元素,就变成 5 条;100 万条文档 × 平均 10 个数组项 = 1000 万条中间文档。这些膨胀后的文档全得进内存、被后续 $match 扫一遍,根本没法走索引。
-
$unwind不是“拆开再筛选”,而是“先无脑复制,再挨个判断” -
$match在$unwind后,MongoDB 只能做文档级扫描(COLLSCAN),totalDocsExamined和nReturned差距极大 - 常见现象:
errmsg: "BSONObj size: 16793600 is invalid",说明中间结果超 16MB 限制
正确顺序:$match 必须在 $unwind 前,且字段必须原始存在
只有原始集合里就有的字段(比如 status、timestamp),才能让前置 $match 触发索引扫描。你不能指望它去过滤 $unwind 后才出现的 "tags.0" 这类路径——那得等展开完才有。
- 错误写法:
[{"$unwind":"$items"}, {"$match": {"items.status": "failed"}}] - 正确写法:
[{"$match": {"items.status": "failed"}}, {"$unwind":"$items"}]——前提是items是原始文档字段,且已建索引(如{"items.status": 1}) - 如果
items是嵌套太深或动态生成的,考虑用$filter+$project替代$unwind
复合条件怎么建索引才真正生效
光把 $match 挪到前面不够,索引设计不匹配照样白搭。MongoDB 复合索引遵循“等值查询字段在前、范围查询字段在后”的顺序,且只对最左前缀生效。
- 查
{"userId": "u123", "createdAt": {"$gte": ISODate("...")}}→ 索引必须是{userId: 1, createdAt: 1},反过来无效 - 查
{"status": "error", "tags": {"$in": ["auth", "db"]}}→{status: 1, tags: 1}才能覆盖 - 用
$expr或$size的$match(如{"$expr": {"$gt": [{"$size": "$items"}, 5]}})基本不走索引,前置也没意义
验证它到底有没有走索引
别信直觉,跑 explain("executionStats") 看真实执行路径:
- 如果
executionStages.stage === "IXSCAN",且totalKeysExamined接近nReturned,说明索引生效 - 如果
executionStages.stage === "COLLSCAN",或者totalDocsExamined是nReturned的几十倍,说明$match没前置或没索引 - 注意:explain 输出里
stage字段可能显示"LIMIT"或"SORT",但关键要看最外层executionStages的根节点类型
最容易被忽略的是:$unwind 前的 $match 即使语法正确,如果字段类型不一致(比如字符串 ID 当成 ObjectId 查)、或用了 $regex 开头通配,索引也会静默失效——这时候 explain 是唯一可信依据。



















