Something Else
Something Else

Reputation: 135

$group inner array values without $unwind

I want to group objects in the array by same value for specified field and produce a count.

I have the following mongodb document (non-relevant fields are not present).

{
  arrayField: [ 
    { fieldA: value1, ...otherFields }, 
    { fieldA: value2, ...otherFields },
    { fieldA: value2, ...otherFields } 
  ],
  ...otherFields
}

The following is what I want.

{
  arrayField: [ 
    { fieldA: value1, ...otherFields }, 
    { fieldA: value2, ...otherFields },
    { fieldA: value2, ...otherFields } 
  ],
  newArrayField: [ 
    { fieldA: value1, count: 1 }, 
    { fieldA: value2, count: 2 },
  ],
  ...otherFields
}

Here I grouped embedded documents by fieldA.

I know how to do it with unwind and 2 group stages the following way. (irrelevant stages are ommited)

Concrete example

// document structure
{
  _id: ObjectId(...),
  type: "test",
  results: [ 
    { choice: "a" }, 
    { choice: "b" },
    { choice: "a" } 
  ]
}
db.test.aggregate([
{ $match: {} },
{
  $unwind: {
    path: "$results",
    preserveNullAndEmptyArrays: true
  }
},
{
  $group: {
    _id: {
      _id: "$_id",
      type: "$type",
      choice: "$results.choice",
    },
    count: { $sum: 1 }
  }
},
{
  $group: {
    _id: {
      _id: "$_id._id",
      type: "$_id.type",
      result: "$results.choice",
    },
    groupedResults: { $push: { count: "$count", choice: "$_id.choice" } }
  }
}
])

Upvotes: 5

Views: 1145

Answers (2)

Ashh
Ashh

Reputation: 46491

You can use below aggregation

db.test.aggregate([
  { "$addFields": {
    "newArrayField": {
      "$map": {
        "input": { "$setUnion": ["$arrayField.fieldA"] },
        "as": "m",
        "in": {
          "fieldA": "$$m",
          "count": {
            "$size": {
              "$filter": {
                "input": "$arrayField",
                "as": "d",
                "cond": { "$eq": ["$$d.fieldA", "$$m"] }
              }
            }
          }
        }
      }
    }
  }}
])

Upvotes: 2

sheilak
sheilak

Reputation: 5873

The below adds a new array field, which is generated by:

  1. Using $setUnion to get unique set of array items, with inner $map to extract only the choice field
  2. Using $map on the unique set of items, with inner $reduce on the original array, to sum all items where choice matches

Pipeline:

db.test.aggregate([{
  $addFields: {
    newArrayField: {
      $map: {
        input: {
          $setUnion: [{
              $map: {
                input: "$results",
                in: { choice: "$$this.choice" }
              }
            }
          ]
        },
        as: "i",
        in: {
          choice: '$$i.choice',
          count: {
            $reduce: {
              input: "$results",
              initialValue: 0,
              in: { 
                $sum: ["$$value", { $cond: [ { $eq: [ "$$this.choice", "$$i.choice" ] }, 1, 0 ] }]
              }
            }
          }
        }
      }
    }
  }
}])

The $reduce will iterate over the results array n times, where n is the number of unique values of choice, so the performance will depend on that.

Upvotes: 1

Related Questions