Solution
The following pipeline should give you the desired result
db.getCollection('account').aggregate(
[
{
$project: {
_id: '$product',
fields: [
{ name: { $literal: 'price' }, value: '$price', count: { $literal: 0 } },
{ name: { $literal: 'sale' }, value: '$sale', count: { $literal: 0 } },
{ name: { $literal: 'count' }, value: { $literal: 0 }, count: '$count' }
]
}
},
{
$unwind: {
path: '$fields'
}
},
{
$match: {
'fields.value': {
$exists: true
}
}
},
{
$group: {
_id: {
product: '$_id',
field: '$fields.name'
},
value: {
$last: '$fields.value'
},
count: {
$sum: '$fields.count'
}
}
},
{
$project: {
_id: '$_id.product',
price: {
$cond: { if: { $eq: [ '$_id.field', 'price' ] }, then: '$value', else: null }
},
sale: {
$cond: { if: { $eq: [ '$_id.field', 'sale' ] }, then: '$value', else: null }
},
count: {
$cond: { if: { $eq: [ '$_id.field', 'count' ] }, then: '$count', else: 0 }
}
}
},
{
$group: {
_id: '$_id',
price: {
$max: '$price'
},
sale: {
$max: '$sale'
},
count: {
$sum: '$count'
}
}
}
])
Explanation
It first creates a new array with elements per field which contain the field name, field value and count value.
Note that the field count
is treated special as it should be accumulated instead of getting the last value of it.
So after the first stage documents look like this:
/* 1 */
{
"_id" : "2",
"fields" : [
{
"name" : "price",
"value" : 5400,
"count" : 0.0
},
{
"name" : "sale",
"count" : 0.0
},
{
"name" : "count",
"value" : 0.0,
"count" : 1
}
]
}
/* 2 */
{
"_id" : "2",
"fields" : [
{
"name" : "price",
"count" : 0.0
},
{
"name" : "sale",
"value" : 0.2,
"count" : 0.0
},
{
"name" : "count",
"value" : 0.0,
"count" : 0
}
]
}
/* 3 */
{
"_id" : "2",
"fields" : [
{
"name" : "price",
"count" : 0.0
},
{
"name" : "sale",
"count" : 0.0
},
{
"name" : "count",
"value" : 0.0,
"count" : 1
}
]
}
It then unwinds the array and filters it to get rid of null values, so after stage 2 & 3 documents look like this:
/* 1 */
{
"_id" : "2",
"fields" : {
"name" : "price",
"value" : 5400,
"count" : 0.0
}
}
/* 2 */
{
"_id" : "2",
"fields" : {
"name" : "count",
"value" : 0.0,
"count" : 1
}
}
/* 3 */
{
"_id" : "2",
"fields" : {
"name" : "sale",
"value" : 0.2,
"count" : 0.0
}
}
/* 4 */
{
"_id" : "2",
"fields" : {
"name" : "count",
"value" : 0.0,
"count" : 0
}
}
/* 5 */
{
"_id" : "2",
"fields" : {
"name" : "count",
"value" : 0.0,
"count" : 1
}
}
In the fourth stage the fields' last values and sum of count
are built. Result looks like this:
/* 1 */
{
"_id" : {
"product" : "2",
"field" : "sale"
},
"value" : 0.2,
"count" : 0.0
}
/* 2 */
{
"_id" : {
"product" : "2",
"field" : "count"
},
"value" : 0.0,
"count" : 2
}
/* 3 */
{
"_id" : {
"product" : "2",
"field" : "price"
},
"value" : 5400,
"count" : 0.0
}
As the values are now in separate documents with a different shape than our desired result should be, we need to project them back into something we can eventually group. So in after the fifth stage documents are like so:
/* 1 */
{
"_id" : "2",
"count" : 0.0,
"price" : null,
"sale" : 0.2
}
/* 2 */
{
"_id" : "2",
"count" : 2,
"price" : null,
"sale" : null
}
/* 3 */
{
"_id" : "2",
"count" : 0.0,
"price" : 5400,
"sale" : null
}
The final stage then just aggregates those documents per product.