最高效的MongoDB聚合:Unwind+Filter方案对比与效率疑问
结论先行
方案4(先$match筛选文档 → $filter过滤数组 → $unwind展开)是四种方案里效率最高的,也是生产环境中最推荐的写法。
逐个方案分析
方案1(先$unwind再$match)
完全不推荐。它会先把集合中所有文档的所有数组元素全部展开,再筛选符合条件的元素。当集合规模大、数组元素多的时候,会产生巨量的中间数据,内存和IO消耗极高,属于典型的“先做无用功再筛选”。方案2(match-unwind-match)
比方案1合理,但仍有浪费。第一步$match先过滤出至少包含一个符合条件数组元素的文档,缩小了后续处理范围;但第二步$unwind会把这些文档的所有数组元素(包括不符合条件的)全部展开,第三步再筛选,相当于做了大量无用的展开操作,额外消耗资源。方案3(先$filter再$unwind)
比方案1、2更高效,但存在缺陷。用$filter先把每个文档的数组过滤为仅包含符合条件的元素,再展开,避免了无用元素的展开;但它没有前置的$match筛选,会处理集合中所有文档——哪怕某个文档的数组里完全没有符合条件的元素,也会白做一次$filter操作,浪费计算资源。方案4(match→filter→unwind)
完美结合了前三者的优势:- 第一步$match利用多键索引(如果已创建
myArrayField.myCriteriaField的索引)快速筛选出包含目标元素的文档,直接排除绝大多数无关文档,把后续处理的数据集降到最小; - 第二步$filter仅保留每个目标文档中符合条件的数组元素,彻底避免了对无效元素的处理;
- 第三步$unwind只展开已经过滤后的有效数组元素,全程没有多余操作,资源消耗最低。
- 第一步$match利用多键索引(如果已创建
影响效率的核心变量
- 集合大小:集合越大,前置$match的“裁剪”作用越明显,能直接减少后续需要处理的文档数量;
- 数组元素数量:数组元素越多,$unwind的开销越大,先$filter缩小数组规模的优势就越突出;
- 索引配置:给
myArrayField.myCriteriaField创建多键索引是关键——没有索引的话,第一步$match会做全集合扫描,效率骤降; - 文档体积:如果文档包含其他大字段(比如大文本、二进制数据),前置$match能减少后续需要加载和处理的大文档数量,进一步降低开销。
是否存在更优实现?
方案4已经是非常高效的写法,不过如果需要保留原文档的所有字段,可以用$addFields替代$project($project默认只保留指定字段,需要手动列出其他字段,而$addFields会自动保留所有原有字段,仅替换目标数组字段),写法更简洁:
db.collection.aggregate([ { $match: { "myArrayField.myCriteriaField": "myValue" } }, { $addFields: { myArrayField: { $filter: { input: "$myArrayField", as: "element", cond: { $eq: ["$$element.myCriteriaField", "myValue"] } } } } }, { $unwind: "$myArrayField" } ])
这种写法的执行效率和方案4几乎一致,只是更适配需要保留原文档全部字段的场景。
内容的提问来源于stack exchange,提问作者peter

