$push本身不控制数组大小,必须配合$each+$slice才能自动截断;单独使用$push会导致数组无界增长,引发内存、性能及存储瓶颈,且$slice在无$each时无效。

直接结论:$push 本身不控制数组大小,必须配合 $each + $slice 才能自动截断;光用 $push 就等于放任数组无界增长,迟早触发内存、性能或存储瓶颈。
为什么 $push 默认会导致数组失控?
MongoDB 的 $push 只负责“追加”,不做任何清理或限制。哪怕你每天调用 100 次 $push 往 logs 字段加一条记录,一年后就是 3.6 万条——文档体积暴涨、查询变慢、备份拉长、甚至触发 errmsg: "Sort exceeded memory limit" 错误。
- 数组字段无法被索引覆盖,每次读取都得完整加载解析,CPU 和内存压力直线上升
-
$push单独使用时,$slice不生效(必须和$each组合) - 如果新数据时间戳更早,而你只用
$push+$slice(没配$sort),结果会截掉最新的几条——子集变成“最老的 N 条”
$push + $each + $slice 的正确写法
这是唯一能原子级实现“追加并限长”的组合,且从 MongoDB 4.4 起稳定支持:
db.users.updateOne(
{ _id: ObjectId("...") },
{ $push: {
recent_ips: {
$each: ["192.168.1.1"],
$slice: -100
}
}
}
)
-
$each必须是数组形式,哪怕只推一个值也要写成["192.168.1.1"] -
$slice: -100表示只保留最后 100 个元素;正数如100表示保留前 100 个 - 这个操作是原子的,不会因并发写入产生中间态超长数组
- 注意:
$slice只截长度,不保证顺序——如果顺序关键,必须加$sort
需要保序时,必须显式加 $sort
比如要按时间倒序存最近 100 条登录记录,只靠 $push + $slice 不够,因为插入顺序 ≠ 逻辑顺序:
db.users.updateOne(
{ _id: ObjectId("...") },
{ $push: {
login_history: {
$each: [{ ip: "192.168.1.1", ts: new Date() }],
$sort: { ts: -1 },
$slice: -100
}
}
}
)
-
$sort是对象,不是字符串;键名必须和文档字段完全一致(如"log.time"就得写{"log.time": -1}) -
$sort和$slice必须和$each在同一层级,顺序无关(MongoDB 内部固定执行:插入 → 排序 → 截断) - 该语法仅在 MongoDB 4.4+ 支持;4.2 及以下只能靠应用层排序 + 两次更新,或改用
$addToSet(但会去重,不适用日志类场景)
聚合阶段中 $push 的数组膨胀风险
聚合管道里的 $push 更危险——它常用于 $group,一旦上游没控量,很容易把几千条记录塞进一个数组,直接触发内存溢出:
- 错误写法:
{$group: { _id: "$user_id", orders: {$push: "$order"} }}—— 全量堆积,毫无节制 - 安全做法:在
$group前加$sort+$limit,例如只取每个用户最新 50 条订单再$push - 若业务允许,优先用
$push收集 ID 列表,再用find({ _id: { $in: [...] } })查详情并由应用层排序——避开 MongoDB 内存瓶颈 -
allowDiskUse: true能绕过内存限制,但它只是“不崩”,不解决根本问题;且在事务中不可用
真正难的不是写对语法,而是判断“这个数组到底要不要全量存”。很多团队直到文档平均体积突破 16MB(BSON 上限)才意识到——$push 从来不是存储方案,只是临时拼接手段。


















