$unwind默认丢弃空数组、null及缺失字段的文档,加preserveNullAndEmptyArrays:true仅保留但统一为tags:null,无法区分原始状态;Go反序列化需为聚合结果单独定义非切片结构体;防丢关键是在$unwind前用$ifNull兜底并校验数据量。

$unwind默认丢弃空数组和null字段
因为 $unwind 的默认行为是:当指定路径(如 "$tags")对应空数组 []、null 或字段完全缺失时,该文档**不会进入后续管道阶段**——直接被过滤掉。这不是 bug,而是设计如此。
- 你执行
{"$unwind": "$tags"},而某文档是{ _id: 1, name: "A" }(无tags字段),它不会输出任何结果 - 同理,
{ _id: 2, tags: [] }或{ _id: 3, tags: null }也全被跳过 - 这种“静默丢弃”在聚合中很难察觉,尤其当你只查部分样本时,容易误判为数据不存在
preserveNullAndEmptyArrays: true 不是万能开关
加了 preserveNullAndEmptyArrays: true 确实能让空/缺失文档继续流下去,但要注意它只是“保命”,不解决语义问题:
- 对
tags: [],它会生成一条{ ..., tags: null }文档(不是空数组,是null值) - 对缺失字段或
tags: null,同样输出tags: null,无法区分原始状态 - 如果后续阶段有
{"$match": {"tags.name": "x"}},这条tags: null仍会被过滤——null没有name字段
Go 结构体反序列化时因类型错配导致字段为 nil
在 Go(如 mgo 或 mongo-go-driver)中,若原始文档结构体定义 Tags []Tag,但 $unwind 后每个输出文档的 tags 是单个对象或 null,BSON 解码器就会失败:
- 解码器期望数组,却收到对象 → 跳过赋值,
Tags保持零值(nil切片) - 即使加了
preserveNullAndEmptyArrays: true,解码tags: null到[]Tag仍是非法类型转换 - 正确做法是为聚合结果单独定义结构体,把
Tags改成Tag(非切片),并允许其为nil或用指针接收
真正防丢数据的关键在 $unwind 前兜底
与其依赖 $unwind 的选项,不如在它之前就统一数据形态:
- 用
$ifNull给缺失/空数组设默认值:{"tags": {"$ifNull": ["$tags", []]}} - 再用
$cond或$size过滤掉明显无意义的长空数组(比如{"$expr": {"$gt": [{"$size": "$tags"}, 100]}}) - 如果业务允许跳过空数组文档,那就别设
preserveNullAndEmptyArrays,反而更干净——避免后续阶段反复处理null
实际最容易被忽略的点是:$unwind 的丢弃行为发生在整个文档粒度,而你调试时往往只看一条样例文档,根本意识不到上游已经筛掉了 30% 的记录。 必须在聚合开头加 $count 或用 $facet 对比前后数量。











