$unwind本身不占内存,但会引爆后续$sort、$group、$lookup等阶段的内存需求,因其将数组展开为多文档,导致数据量激增且未裁剪字段时全量加载入内存。

$unwind本身不占内存,但会引爆后续阶段的内存需求
单看$unwind阶段,它只是把一个数组展开成多条文档,并不直接消耗大量内存。真正出问题的是它之后紧跟着的$sort、$group或$lookup——这些阶段必须把所有展开后的文档全加载进内存才能工作。
比如原始集合有 1 万条文档,每条含 tags: ["a", "b", "c"](3 个元素),$unwind: "$tags"后变成 3 万条;如果紧接着$group: { _id: "$tags", count: { $sum: 1 } },mongos 就得把这 3 万条全塞进内存做分组。若原始文档还带content字段(平均 50KB),那光是展开前就已拖入 500MB 内存,根本撑不到$group执行。
- 默认
$unwind会静默丢弃空数组/null/缺失字段文档,导致你误以为数据量小,实际聚合时因preserveNullAndEmptyArrays: true补全后数据量翻倍甚至十倍 - 嵌套越深、数组越长,
$unwind后的文档爆炸越剧烈;例如comments.replies两层嵌套,100 条原始文档可能产出上万中间行 - 在分片集群中,
$unwind发生在各分片本地,但$group或$sort需由mongos合并结果——此时内存压力从分片转移到mongos,更容易 OOM
为什么$unwind + $group最容易爆内存
$group阶段要求所有待分组文档必须驻留内存,而$unwind常使文档数激增且无法索引加速。MongoDB 不会对$unwind输出做自动去重或采样,也不会跳过未参与分组的字段——只要前面没$project裁剪,整个原始文档结构(包括大文本、二进制)都会被复制进内存。
- 错误写法:
{ $unwind: "$items" }→{ $group: { _id: "$items.category", total: { $sum: "$items.price" } } },却没提前$project只留items.category和items.price - 正确做法:在
$unwind前或后立刻加$project,显式限定字段,如{ items: { category: 1, price: 1 } },并设_id: 0 - 若只需统计数量,别用
$unwind:改用$size或$reduce在原数组上计算,避免展开
preserveNullAndEmptyArrays=true 是双刃剑
加preserveNullAndEmptyArrays: true能防止数据丢失,但它会让原本被过滤掉的空/缺失文档“复活”为一条null占位记录——这看似安全,实则悄悄放大了后续阶段的输入规模。
- 假设 10 万条订单中 2 万条
logs: [],默认$unwind: "$logs"只处理剩下 8 万条;开启preserveNullAndEmptyArrays后,变成 10 万条null记录进入管道 - 这些
null记录仍会参与$sort(排在最前或最后)、$group(形成_id: null组),同样占用内存和 CPU - 若业务逻辑允许,优先在
$match里排除空数组:{ logs.0: { $exists: true } },比靠$unwind兜底更轻量
分片集群下$unwind的隐藏陷阱
在分片环境中,$unwind虽在分片本地执行,但它的副作用会传导到mongos层:一旦后续有$sort或跨分片$group,mongos就得拉取所有分片的展开结果并合并。这时$unwind造成的文档膨胀,会直接压垮mongos内存。
- 务必确认分片键是否参与前置
$match:例如按shop_id分片,聚合开头就必须有{ $match: { shop_id: "abc" } },否则$unwind会在所有分片上运行,数据量乘以分片数 - 避免
$unwind后接$lookup:尤其当$lookup目标集合未按相同分片键路由时,会触发广播查询,中间数据量呈指数级增长 - 临时目录空间不足也会让
allowDiskUse: true失效——mongos默认写/tmp,而容器环境常是内存盘(tmpfs),256MB 瞬间填满
$unwind本身,而是它像一根导火索,把前期没控制住的数据体积、没建对的索引、没设好的分片路由,全在后续阶段集中引爆。多数人调优时只盯着$group报错,却忘了回看$unwind前有没有$match过滤、有没有$project裁剪、有没有确认分片键是否生效。











