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

MongoDB 聚合用 $values.0 取数组元素却得到空数组

聚合投影中的数字点号路径不表示数组下标。使用 $arrayElemAt 取指定位置,并区分越界、空数组和字段类型不符。

MongoDB 聚合中写出 { $project: { first: "$values.0" } },原字段 values 明明是有内容的数组,结果里的 first 却成了空数组。原因是这里的字符串按聚合字段路径解释,不能把末尾 .0 当作数组下标。

下面以 MongoDB 8.0 的聚合行为和 mongosh 写法说明,处理的是“每个文档取数组的一个元素”。示例仅查询,不修改集合;sample_items、sampleKey 和字段名需要换成项目实际名称。

先确认投影前的字段还在

截断管道到出问题的投影之前,查看一条已知文档:

db.sample_items.aggregate([
  { $match: { sampleKey: "EXAMPLE-001" } },
  { $project: { values: 1 } },
  { $limit: 1 }
])

若 values 在前一个阶段已经被删除、改名或变成对象,修下标写法仍不足以解决问题。先确认它确实是数组,以及目标位置有没有元素。

不要把查询条件里按位置访问数组的点号写法,直接搬进聚合表达式。"$values.0" 也不是 JavaScript 数组访问表达式,不会因为写在 mongosh 里就按 JavaScript 的下标规则求值。

使用 arrayElemAt 指定数组和位置

db.sample_items.aggregate([
  { $match: { sampleKey: "EXAMPLE-001" } },
  {
    $project: {
      first: { $arrayElemAt: ["$values", 0] },
      second: { $arrayElemAt: ["$values", 1] }
    }
  },
  { $limit: 1 }
])

对于 values: ["north", "south"],预期 first 为 "north",second 为 "south"。$arrayElemAt 的第一个参数是数组表达式,第二个参数是整数位置:零表示第一项,负数从末尾计数,例如 -1 表示最后一项。

这里只需取一个位置,无需先展开数组再分组。额外的 $unwind 会改变中间文档数量;为了取“第一项”而先排序,还会改变数组原本的先后含义。

空数组和非数组要按接口规则区分

下标超出数组范围时,$arrayElemAt 不返回结果;在上述投影中,对应字段可能不存在。数组表达式指向未定义字段时,结果为 null。因此,不能仅用“字段没输出”反推原数组一定不存在。

如果集合允许字段类型不齐,可以在取元素之前显式分支,而不是批量把原字段改成空数组:

db.sample_items.aggregate([
  { $match: { sampleKey: "EXAMPLE-001" } },
  {
    $project: {
      valuesIsArray: { $isArray: "$values" },
      first: {
        $cond: {
          if: { $isArray: "$values" },
          then: { $arrayElemAt: ["$values", 0] },
          else: null
        }
      }
    }
  },
  { $limit: 1 }
])

这份诊断投影保留 valuesIsArray,避免把类型错误与合法空数组混为一谈。它把非数组分支输出为 null,这是示例选择的输出约定,不是数据已经修复;空数组的取值分支仍可能不输出 first。正式接口若需要固定字段或不同错误标记,应先确定约定,再调整投影。

修复后用两元素数组、单元素数组、空数组和缺失字段检查输出内容及字段是否存在。若数组元素自身也是对象,应先取出对象,再处理对象内的字段,不要再次把数字点号路径当下标。

保存原管道和接口返回样本即可回退查询,无需重写数组。尤其不要为了绕开投影问题把数组排序后存回数据库,那会永久改变元素顺序,超出本次修复范围。