$addField用于为文档添加新字段,等价于$project显式指定所有字段,MongoDB 4.2起$set为其别名;若字段名重复则覆盖原值,支持嵌套、表达式及点符号操作。

聚合中不能直接用变量当字段名,得靠 $objectToArray + $arrayElemAt
MongoDB 的 $project 或 $addFields 不支持把字符串变量(比如存在文档里的 fieldName 字段)直接当键名来用。硬写 {"$fieldName": "$value"} 会报错或静默忽略——这不是语法错误,而是根本没这机制。
真正可行的路径是:先把目标对象转成键值对数组,再按需提取。典型场景是你有一组文档,其中某个字段(如 metadata)结构不固定,但你知道要取的键名藏在另一个字段里(比如 targetKey):
{
"metadata": { "status": "active", "priority": 2 },
"targetKey": "status"
}
这时必须走两步:
- 用
$objectToArray把metadata转成[{k:"status",v:"active"},{k:"priority",v:2}] - 用
$filter+$eq找出k === targetKey的元素,再用$arrayElemAt取出第一个匹配项的v
用 $let 封装逻辑,避免重复写长表达式
如果多个地方都要按动态 key 取值(比如同时取 targetKey 和 backupKey),重复写 $filter 嵌套会极难读且易错。推荐用 $let 提前算好中间结果:
{
$let: {
vars: {
metadataPairs: { $objectToArray: "$metadata" },
targetVal: {
$arrayElemAt: [
{ $map: {
input: { $filter: { input: "$$metadataPairs", cond: { $eq: ["$$this.k", "$targetKey"] } } },
as: "pair",
in: "$$pair.v"
}
},
0
]
}
},
in: { dynamicValue: "$$targetVal" }
}
}
注意点:
-
$arrayElemAt返回null如果没匹配项,不会报错,但你要决定是否加$ifNull处理默认值 -
$map在这里只是把filter结果的每个{k,v}映射为v,比嵌套$reduce更直观 - 别漏掉
$$——$$metadataPairs是变量引用,$metadataPairs会被当成字段路径解析
重命名动态字段时,$replaceRoot 比 $project 更灵活
如果你最终想把动态取出来的值,挂到根文档下并用新名字(比如把 targetKey 对应的值变成 currentStatus),不要试图在 $project 里拼字段名。正确做法是先构造一个新对象,再用 $replaceRoot 替换整个文档:
{
$replaceRoot: {
newRoot: {
$mergeObjects: [
"$$ROOT",
{ currentStatus: "$$targetVal" }
]
}
}
}
这样做的好处:
- 保留原始所有字段(
$$ROOT),只叠加新字段,不用逐个列旧字段 - 避免
$project中漏写字段导致数据丢失 - 如果要删旧字段(比如删掉
targetKey),可在$mergeObjects前用$unset预处理
性能和兼容性:4.2+ 才有 $function,但不建议用
有人想到用 $function 写 JS 逻辑动态拼 key,虽然语法上可行,但实际踩坑多:
- 服务器端 JS 执行慢,且无法利用索引
- 集群环境下可能因 JavaScript 引擎差异导致行为不一致
- MongoDB 官方明确标注
$function为“高级功能”,生产环境慎用 - 4.2 以下版本根本不支持,而
$objectToArray从 3.4.4 就已稳定
真正需要高频动态字段操作的场景,往往说明数据模型本身值得重新评估——比如把 metadata 拆成标准化字段,或用应用层处理代替全在聚合里硬扛。


















