$replaceroot报“requires a document input”是因为它严格要求输入必须是单个对象,而上游阶段(如$lookup、$group)常输出数组,导致类型不匹配;正确做法是先$unwind展开或用$arrayelemat取首项转为对象。

直接用 $replaceRoot 是最干净的方式,但必须确保输入是单个文档(不是数组),否则会报错 "$replaceRoot requires a document input"。
为什么 $replaceRoot 会报 “requires a document input”?
这个错误几乎总是因为上游阶段(比如 $lookup 或 $group)输出了数组,而 $replaceRoot 只接受单个对象作为输入。
- 常见场景:
$lookup返回items: [{...}, {...}],你却对整个数组执行$replaceRoot: { newRoot: "$items" } - 正确做法:先用
$unwind展开数组,再对每个子文档做$replaceRoot - 如果只想取第一个匹配项,可用
$arrayElemAt: ["$items", 0]提前转成对象,再传给$replaceRoot
$replaceRoot 和 $mergeObjects 混用的典型写法
很多时候你不是想“完全替换根”,而是想把子文档字段“提上来”,同时保留原根的部分字段(比如 _id)。这时不能只写 $replaceRoot: { newRoot: "$subdoc" },否则原 _id 就丢了。
- 安全做法:
$replaceRoot: { newRoot: { $mergeObjects: ["$subdoc", { _id: "$_id" }] } } - 注意顺序:
$mergeObjects后面的对象字段会覆盖前面的同名字段,所以把"$subdoc"放前面、自定义字段放后面 - 如果
subdoc本身可能为null,加个$ifNull防崩:$ifNull: ["$subdoc", {}]
替代方案:不用 $replaceRoot,改用 $project 手动展开
当结构简单、字段明确时,$project 更直观,也更容易调试。
- 例如子文档在
user.profile,想提上所有字段:{ name: "$user.profile.name", email: "$user.profile.email", age: "$user.profile.age" } - 优势:不依赖输入是否为对象,不会因空值崩溃;劣势:字段多时维护成本高,无法动态处理未知字段
- 若要保留原字段又不想写死,可组合
$objectToArray+$arrayToObject,但性能较差,仅限必要场景
真正容易被忽略的是:MongoDB 的聚合管道是逐文档处理的,$replaceRoot 不会“批量重写整个集合结构”,它只作用于当前文档的当前形态。一旦上游逻辑没理清数据形状,后续所有操作都会连锁出错。











