开启 preserveNullAndEmptyArrays: true 可保留空数组、null 和缺失字段的文档,否则 $unwind 默认静默丢弃这三类文档;正确写法为 {"$unwind": {"path": "$tags", "preserveNullAndEmptyArrays": true}},且后续匹配应使用 {"tags.0": {"$exists": true}} 区分真实非空数组。

开启 preserveNullAndEmptyArrays: true 就能保住空数组、null 和缺失字段的文档,否则默认全丢。
$unwind 默认会静默丢弃哪些文档?
不加任何选项时,$unwind 只处理“合法非空数组”:一旦遇到以下任一情况,整条原始文档就彻底不出现在结果里:
-
"tags": [](空数组) -
"tags": null(显式 null) - 文档根本没
tags字段(字段缺失)
这不是 bug,是设计行为——MongoDB 认为这些“无效数组源”不该参与展开。但业务上往往需要保留它们做后续判断或补全,所以必须显式干预。
正确写法:path + preserveNullAndEmptyArrays 必须放在对象内
常见错误是把 preserveNullAndEmptyArrays 写在 $unwind 阶段外,或者类型写错:
- ❌ 错误:
{"$unwind": "$tags", "preserveNullAndEmptyArrays": true}→ 该参数不在$unwind对象里,被忽略 - ❌ 错误:
{"$unwind": {"path": "$tags", "preserveNullAndEmptyArrays": "true"}}→ 值必须是布尔型true,不是字符串 - ✅ 正确:
{"$unwind": {"path": "$tags", "preserveNullAndEmptyArrays": true}}
注意:PyMongo、Node.js driver、mongosh 全部要求这个布尔值是原生 true,传 1 或 "true" 都会失效。
开启后三种情况都变成 tags: null,怎么区分“真有数据”?
启用 preserveNullAndEmptyArrays: true 后,空数组、null、字段缺失三者统一输出为 tags: null。如果后续要筛出“至少有一个真实标签”的文档,不能用:
- ❌
{"$match": {"tags": {"$exists": true}}}→ 会把null和缺失也当成“存在” - ✅ 正确:
{"$match": {"tags.0": {"$exists": true}}}→ 检查数组第一个元素是否存在(只有非空数组才满足) - ✅ 或:
{"$match": {"tags": {"$ne": null}}}→ 显式排除null,但注意这仍会放过缺失字段(因缺失字段不等于null)
最稳妥的写法是组合判断:{"$match": {"tags.0": {"$exists": true}}},它只对真正含元素的数组生效。
Go / Python 等语言中结构体映射容易踩的坑
展开后每条文档的 tags 字段不再是数组,而是单个对象或 null。如果结构体还定义成 Tags []Tag,反序列化时就会把 null 当空切片,或直接 panic。
- Go 中应定义为
Tags *Tag或Tags Tag(配合零值处理),而非切片 - Python 的 PyMongo 返回的是字典,需手动检查
doc.get("tags")是否为None或字典,不能直接当列表遍历 - 尤其注意:即使开了
preserveNullAndEmptyArrays,tags字段在展开后也绝不会是数组 —— 它要么是对象,要么是null
这个语义错位是最隐蔽的问题:shell 里看着像“多条记录”,代码里却要按单值解构,稍不留意就 panic 或逻辑跳过。


















