做数据统计的时候,经常需要把某一组文档里的多个值收集到一个数组中。比如统计每个用户的订单列表、每个班级的学生名单,这类需求用普通的find查询很难一步到位,而MongoDB聚合管道里的$push操作符正是为此设计的。它属于累积操作符的一种,专门在$group阶段使用,能够把分组内所有文档的指定字段值依次推入一个数组,最终返回一个完整的数组结果。这篇文章就来详细拆解$push的用法,包括基本语法、推入完整文档、结合其他管道阶段的组合技巧,以及和$addToSet的区别。

$push的基本语法与工作原理
$push在聚合管道中只能出现在$group阶段的累积器位置,语法结构非常简单:{ $push: <表达式> }。表达式可以是一个字段引用,比如"$productId",也可以是一个对象字面量,甚至可以是嵌套的表达式组合。MongoDB在执行$group时,会遍历输入文档流,遇到相同分组键的文档,就把表达式的计算结果追加到对应的数组里,顺序与文档进入管道的顺序一致。
先准备一份测试数据,模拟一个订单集合:
db.orders.insertMany([
{ user: "张三", product: "键盘", amount: 299 },
{ user: "张三", product: "鼠标", amount: 99 },
{ user: "李四", product: "显示器", amount: 1899 },
{ user: "李四", product: "耳机", amount: 499 },
{ user: "王五", product: "摄像头", amount: 399 }
]);
如果想知道每个用户买了哪些商品,用$push配合$group就能一次查出来:
db.orders.aggregate([
{
$group: {
_id: "$user",
products: { $push: "$product" },
totalAmount: { $sum: "$amount" }
}
}
]);
返回结果中,每个用户对应一个products数组,同时可以用$sum并行统计消费总额。这种写法的好处是一条聚合语句同时完成分组、收集和求和,避免了在应用层再写循环逻辑。
推入完整文档与自定义结构
$push后面接的表达式不局限于单个字段,还可以推入整个文档或者手工构造的对象。把表达式写成$$ROOT就能把原始文档完整收进数组:
db.orders.aggregate([
{
$group: {
_id: "$user",
orderList: { $push: "$$ROOT" }
}
}
]);
这种方式在需要保留文档全部字段时很方便,但要注意内存占用,后面会专门讨论。更多时候我们会推入一个自定义结构的对象,只保留关心的字段,这样结果更干净,传输量也更小:
db.orders.aggregate([
{
$group: {
_id: "$user",
orders: {
$push: {
item: "$product",
price: "$amount"
}
}
}
}
]);
这个查询的结果里,每个用户的orders数组元素都是{ item, price }结构。还可以进一步配合$addFields在推入时动态计算新字段,例如给每笔订单加上购买时的折扣价,写法上和普通表达式没有区别。
配合$match和$project实现条件收集
实际业务里往往不需要收集全部文档,只想收集满足条件的部分。常见做法是把$match放在$group之前先过滤,比如只统计金额大于200的订单:
db.orders.aggregate([
{ $match: { amount: { $gt: 200 } } },
{
$group: {
_id: "$user",
bigOrders: { $push: "$product" }
}
},
{ $project: { _id: 0, user: "$_id", bigOrders: 1 } }
]);
如果过滤条件依赖于分组之后的结果,比如只要消费总额超过1000的用户的订单数组,就需要把$match放到$group之后,对统计字段做筛选。管道的执行顺序是从上到下,理解每一步的输入输出是写好聚合的关键。
还有一种更灵活的场景:在同一个分组内,根据文档自身的条件决定是否推入。这可以通过$cond构造条件表达式,让不满足条件时推入一个缺省值,再在后续阶段用$filter清理掉。虽然写法绕一点,但在无法提前过滤分组的情况下是个可行的兜底方案。
$push与$addToSet的区别
很多人容易把$push和$addToSet搞混。两者的核心区别在于:$push保留重复值,数组里出现什么完全取决于输入;而$addToSet会自动去重,只有数组中不存在该值时才会添加。举个例子,如果张三买了两个相同的键盘,用$push的结果是["键盘", "键盘"],用$addToSet的结果则只有一个"键盘"。
选择哪个操作符要看业务语义。统计购买明细时重复记录是有意义的,必须用$push;统计用户接触过哪些品类时只关心去重集合,用$addToSet更合适。另外要注意,$addToSet对元素的相等判断是基于值比较的,数组、文档类型的元素也能正确去重,但元素顺序不同的数组会被视为不相等。
使用$push的注意事项与性能问题
最需要警惕的一点是聚合管道中的内存限制。MongoDB单个聚合阶段的默认内存上限是100MB,$push累积的数组全部驻留在内存中,一旦分组内的文档数量非常大,比如几十万条记录被推入同一个数组,很容易触发内存超限报错。解决办法有几种:一是先用$match、$limit减少进入管道的数据量;二是利用$group阶段允许磁盘溢写的特性,在执行时加上allowDiskUse: true参数;三是从设计上重新审视,是否真的需要一次性取出这么大的数组,分页读取可能是更合理的方案。
db.orders.aggregate(
[
{ $group: { _id: "$user", orderList: { $push: "$$ROOT" } } }
],
{ allowDiskUse: true }
);
另外要区分聚合操作符$push和更新操作符$push。两者名字相同但完全不是一个东西:更新时的$push用于往已有文档的数组字段里追加元素,配合$each、$slice等修饰符使用;聚合时的$push只能在$group里做累积。刚接触MongoDB的同学在查文档时一定要确认自己看的是哪一章,避免混用导致语法报错。
最后提一点实践经验:推入完整文档$$ROOT虽然省事,但会让结果集膨胀得很快,尤其是原文档字段多、体积大的时候。建议在$group之前先用$project裁剪掉不需要的字段,或者在$push表达式里明确列出需要的字段。养成这个习惯后,聚合查询的执行效率和结果的可读性都会有明显提升。
MongoDB聚合管道$push操作符数组操作符修改时间:2026-09-10 17:25:05