$unwind 默认静默丢弃空数组、null或缺失字段的文档,需显式设置preservenullandemptyarrays=true才能输出null占位文档,否则导致数据丢失且不易排查。

默认情况下,$unwind 遇到空数组、null 或缺失字段会直接丢弃整条文档,不是报错,而是静默过滤掉 —— 这是绝大多数人踩坑的起点。
$unwind 遇到空数组时默认不输出文档
比如文档里有 "tags": [] 或 "tags": null 或压根没 tags 字段,用 {"$unwind": "$tags"} 之后,这条文档就从结果里彻底消失了。你查不到它,聚合也不报错,只觉得“数据少了”,但找不到原因。
常见错误现象:
- 聚合结果总数比原始
countDocuments()少很多 - 某些已知含空数组的文档在
$unwind后完全不见 - 后续
$group或$match统计值偏低,且无法对齐业务预期
preserveNullAndEmptyArrays=True 是必须显式加的开关
PyMongo 中必须写成文档形式并设键为 preserveNullAndEmptyArrays(注意大小写和下划线),不能简写,也不能放错层级。
正确写法示例:
pipeline = [
{
"$unwind": {
"path": "$tags",
"preserveNullAndEmptyArrays": True
}
},
{"$project": {"_id": 0, "name": 1, "tag": "$tags"}}
]
错误写法包括:
-
{"$unwind": "$tags", "preserveNullAndEmptyArrays": True}—— 键不在$unwind文档内,无效 -
{"$unwind": {"path": "$tags", "preserveNullAndEmptyArrays": "true"}}—— 值必须是布尔型True,不是字符串 -
{"$unwind": {"path": "$tags", "preserveNullAndEmptyArrays": 1}}—— PyMongo 不接受整数代替布尔值
空数组和 null 字段的行为差异要心里有数
开启 preserveNullAndEmptyArrays=True 后:
-
"tags": []→ 输出一条文档,tags字段为null -
"tags": null→ 输出一条文档,tags字段仍为null - 字段完全缺失 → 输出一条文档,
tags字段为null
也就是说,这三种情况最终都变成同一种状态:tags: null。如果你后续要用 $match 过滤出“有真实标签”的文档,得写成 {"$match": {"tags.0": {"$exists": True}}} 或 {"$match": {"tags": {"$ne": null}}},而不是 {"tags": {"$exists": True}}(后者会把 null 和缺失都算进去)。
includeArrayIndex 能帮你定位原始位置,但别滥用
如果需要知道某条展开后的记录原来在数组里的第几位,可以加 includeArrayIndex:
{"$unwind": {"path": "$items", "includeArrayIndex": "item_idx", "preserveNullAndEmptyArrays": True}}
这样每条展开文档会多一个 item_idx 字段,值为数字(从 0 开始)。但要注意:
- 如果原字段不是数组(比如是字符串或对象),
$unwind会把它当单元素数组处理,item_idx恒为0 - 如果原字段是
null或空数组,开启preserveNullAndEmptyArrays后,item_idx字段不会生成 —— 它只在真正展开数组元素时才写入
所以,item_idx 存在,说明这条记录确实来自非空数组;不存在,不代表有问题,只是来源是空/null/缺失。
真正容易被忽略的点是:preserveNullAndEmptyArrays 不是“让 $unwind 不崩溃”,而是“让它主动输出占位文档”。你得在后续阶段明确处理这些 null,否则它们可能混进统计、分组或导出结果里,造成语义污染。











