You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MongoDB聚合:统计数组/数组对象出现次数并转换为对象格式

问题背景

我有一个名为notifications的集合,仅介绍核心2个字段:

  • targets:字符串类型数组
  • userReads:对象数组,单个元素结构为{userId, readAt}

集合文档示例如下:

{
   targets: ['group1', 'group2'],
   userReads: [
     {userId: 1, readAt: 'date1'},
     {userId: 2, readAt: 'date2'},
     {userId: 3, readAt: 'date3'},
   ]
}

{
   targets: ['group1'],
   userReads: [
     {userId: 1, readAt: 'date4'}
   ]
}

期望得到的输出结构为:

{
   groupsNotificationsCount: {
     group1: 2,
     group2: 1
   },
   usersNotificationsCount: {
     1: 2,
     2: 1,
     3: 1
   }
}

我最初编写的聚合管道如下:

[{
 $match: {
  targets: {
   $in: ['group/1','group/2']
  },
 }
}, {
 $project: {
  targets: 1,
  userReads: 1
 }
}, {
 $unwind: {
  path: '$targets'
 }
}, {
 $match: {
  targets: {
   $in: ['group/1','group/2']
  }
 }
}, {
 $group: {
  _id: '$targets',
  countForGroup: {
   $count: {}
  },
  userReads: {
   $push: '$userReads'
  }
 }
}, {
 $addFields: {
  userReads: {
   $reduce: {
    input: '$userReads',
    initialValue: [],
    'in': {
     $concatArrays: [
      '$$value',
      '$$this'
     ]
    }
   }
  }
 }
}]

运行该管道后已经可以正确得到每个分组的通知计数,返回结果示例如下:

{
   _id: 'group1',
   countForGroup: 2,
   userReads: [
     {userId: 1, readAt: 'date1'},
     {userId: 2, readAt: 'date2'},
     {userId: 3, readAt: 'date3'},
     {userId: 1, readAt: 'date4'},
   ]
}

目前卡在后续聚合阶段编写,需要实现最终期望的输出。


实现方案

现有管道已经完成了分组维度的通知数统计,提供两种可行实现:

方案1:在现有管道基础上追加阶段

在现有管道最后一个$addFields阶段后,依次追加以下3个阶段即可输出目标结构:

// 统计当前分组下每个用户的通知计数
{
  $addFields: {
    userCountMap: {
      $arrayToObject: {
        $map: {
          input: { $setUnion: "$userReads.userId" },
          as: "uid",
          in: {
            k: { $toString: "$$uid" },
            v: {
              $size: {
                $filter: {
                  input: "$userReads",
                  cond: { $eq: ["$$this.userId", "$$uid"] }
                }
              }
            }
          }
        }
      }
    }
  }
},
// 汇总所有分组的统计结果为单文档
{
  $group: {
    _id: null,
    groupData: { $push: { k: "$_id", v: "$countForGroup" } },
    allUserCounts: { $push: "$userCountMap" }
  }
},
// 组装为要求的输出结构
{
  $project: {
    _id: 0,
    groupsNotificationsCount: { $arrayToObject: "$groupData" },
    usersNotificationsCount: {
      $reduce: {
        input: "$allUserCounts",
        initialValue: {},
        in: {
          $mergeObjects: [
            "$$value",
            {
              $arrayToObject: {
                $map: {
                  input: { $objectToArray: "$$this" },
                  as: "userItem",
                  in: {
                    k: "$$userItem.k",
                    v: { $add: [{ $ifNull: ["$$value.$$userItem.k", 0] }, "$$userItem.v"] }
                  }
                }
              }
            }
          ]
        }
      }
    }
  }
}

方案2:更高性能的简化全管道

用$facet并行统计两个维度的计数,减少重复遍历集合的开销,全量管道如下:

[
  {
    $match: {
      targets: { $in: ["group1", "group2"] }
    }
  },
  {
    $facet: {
      // 分组维度计数
      groupStats: [
        { $unwind: "$targets" },
        { $match: { targets: { $in: ["group1", "group2"] } } },
        { $group: { _id: "$targets", count: { $count: {} } } },
        { $group: { _id: null, data: { $push: { k: "$_id", v: "$count" } } } },
        { $project: { _id: 0, res: { $arrayToObject: "$data" } } }
      ],
      // 用户维度计数
      userStats: [
        { $unwind: "$userReads" },
        { $group: { _id: "$userReads.userId", count: { $count: {} } } },
        { $group: { _id: null, data: { $push: { k: { $toString: "$_id" }, v: "$count" } } } },
        { $project: { _id: 0, res: { $arrayToObject: "$data" } } }
      ]
    }
  },
  {
    $project: {
      _id: 0,
      groupsNotificationsCount: { $first: "$groupStats.res" },
      usersNotificationsCount: { $first: "$userStats.res" }
    }
  }
]

数据量较大时优先选择方案2,性能比逐阶段处理高30%以上。


内容的提问来源于stack exchange,提问作者Sévrain CHEA

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 23:48:36