DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
雨云;给项目一台云服务器
RackNerd;$21.99/年 · 1GB KVM

MongoDB 展开空数组后,聚合结果中的父文档消失

定位结果在多级数组的哪次展开中消失,按业务启用 preserveNullAndEmptyArrays,并核对后续筛选和组合行数。

只执行 $match 能查到订单,再展开 items 也有结果;加上 { $unwind: "$items.discounts" } 后,结果却变成空列表。若筛选出的商品恰好都没有折扣,问题可能出在这一次展开:默认的 $unwind 不为缺失、null 或空数组字段输出文档。

这里讨论支持 preserveNullAndEmptyArrays 选项的 MongoDB 聚合管道,处理的是“没有折扣的商品也要保留”的导出规则。数组中确实没有元素时,默认不输出并非执行失败;是否要改,要看业务是否需要保留父级记录。

找到结果消失的那一级数组

先对一条已知订单截取管道,不要只看最终结果数量。下面是只读检查示例,orders、orderNo 和编号均需换成实际字段和值:

db.orders.aggregate([
  { $match: { orderNo: "ORDER-EXAMPLE-001" } },
  { $unwind: "$items" },
  { $project: { orderNo: 1, "items.sku": 1, "items.discounts": 1 } }
])

查看每个商品的 discounts 是有元素的数组、空数组、null,还是根本不存在。若这个截断管道已经没有输出,不能归因于尚未执行的折扣展开:应继续向前检查 $match 和 items。

非空的非数组值是另一种情况。$unwind 会把它作为单个值处理,因此有输出也不能证明字段类型正确。若模型要求数组,应单独检查和修复写入端,不能用保留空值的选项掩盖类型错误。

在会丢失父记录的展开阶段启用保留

需要让没有折扣的商品仍占一行时,将对应阶段改为对象形式:

db.orders.aggregate([
  { $match: { orderNo: "ORDER-EXAMPLE-001" } },
  { $unwind: "$items" },
  {
    $unwind: {
      path: "$items.discounts",
      preserveNullAndEmptyArrays: true
    }
  },
  { $project: { orderNo: 1, "items.sku": 1, "items.discounts": 1 } }
])

此处只改变折扣的展开策略。外层 items 仍采用默认规则,没有商品的订单依然不会输出。若导出要求连没有商品的订单也保留,需要在外层 $unwind 同样明确这项选项,并确认下游允许缺少商品字段。

开启选项后,空数组或缺失字段会保留父文档,但输出中可能没有该字段;输入为 null 时,字段仍为 null。它不会自动补成空数组或造出一条金额为零的折扣。下游导出格式若要求固定字段,应另行定义缺失值的表示,不能把“没有折扣记录”随意等同于某条真实的零金额记录。

后续展开和筛选仍可能丢行

管道有多个 $unwind 时,各阶段独立生效。在 items.discounts 保留了订单,后面的 payments.refunds 若仍按默认方式展开空数组,订单仍可能消失。逐级检查每个数组的保留要求,比对所有阶段统一加参数更容易发现结果含义是否改变。

同样,后面的 $match 若要求某个折扣字段满足条件,保留下来的无折扣行仍可能被过滤。这时应重新确认筛选意图,而不是以为选项失效。

多个互不依赖的数组连续展开还会产生组合数量。例如两个商品与三笔支付逐项组合,可能出现六行;“行数增加”不等于订单增加。按订单汇总或导出时,要保留能够识别订单和明细的键,避免把展开后的行数当作订单数。

用有折扣和无折扣两条路径验收

在隔离测试数据中,分别准备折扣数组有两项、空数组、null 和字段缺失的商品。按上述管道,前者应输出两行,后三者应分别保留一行;再检查空 items 的订单是否符合既定策略。这里描述的是预期语义,不是已经执行过的测试结果。

本修复只改查询,不需要批量把数据库中的空值改成 [],也不需要补造子记录。保存原管道及导出样本,先确认下游接受新增保留行和缺失字段,再切换查询。回退查询不会撤销已经导出的文件或重复处理过的记录;重新导出前按订单及批次标识核对,避免把两份结果叠加。