I have documents similar to this stored in mongodb
{
"_id":"transaction_id"
"customer":"some customer",
"order_date":Date('2011-01-01'),
"delivery_date":Date('2011-01-15'),
"amt":500.0,
"qty":50
},
{
"_id":"transaction_id"
"customer":"some customer",
"order_date":Date('2011-01-01'),
"delivery_date":Date('2011-02-04'),
"amt":500.0,
"qty":50
}
I am looking to do some aggregation for order date and delivery date to plot the total quantity of inventory being ordered and delivered to each customer per month.
Granted I could run 2 aggregation queries to obtain what I want, but I was just wondering if it is possible to obtain a result that has 2 sets of groups with 1 command?
Expected result would be like:
results:[{
_id:{
customer:"some customer"
},
orders:[
{
year:2011,
month:1,
qty:100
},
...
]
deliveries:[
{
year:2011,
month:1,
qty:50
},
{
year:2011,
month:2,
qty:50
},
...
]
},...]
You can do this in a single query, you just need to be a little creative with manipulating the documents and then essentially do two $group
stages, adding by date first and then by the customer.
So first of all for current MongoDB versions 2.6 and above due to some of the operators used:
db.transactions.aggregate([
// Project an additional array, stands for "order", "delivery"
{ "$project": {
"_id": 0,
"customer": 1,
"order_date": 1,
"delivery_date": 1,
"qty": 1,
"type": { "$literal": ["o","d"] }
}},
// Unwind that array, creates two documents by "type"
{ "$unwind": "$type" },
// Group by "customer", "type" and date
{ "$group": {
"_id": {
"customer": "$customer",
"type": "$type",
"year": {
"$year": {
"$cond": [
{ "$eq": [ "$type", "o" ] },
"$order_date",
"$delivery_date"
]
}
},
"month": {
"$month": {
"$cond": [
{ "$eq": [ "$type", "o" ] },
"$order_date",
"$delivery_date"
]
}
}
},
"qty": { "$sum": "$qty" }
}},
// Group on the "customer" selecting which array to add to
{ "$group": {
"_id": "$_id.customer",
"orders": {
"$push": {
"$cond": [
{ "$eq": [ "$_id.type", "o" ] },
{
"year": "$_id.year",
"month": "$_id.month",
"qty": "$qty"
},
false
]
}
},
"deliveries": {
"$push": {
"$cond": [
{ "$eq": [ "$_id.type", "d" ] },
{
"year": "$_id.year",
"month": "$_id.month",
"qty": "$qty"
},
false
]
}
}
}},
// Getting rid of the `false` values in there
{ "$project": {
"orders": { "$setDifference": [ "$orders", [false] ] },
"deliveries": { "$setDifference": [ "$deliveries", [false] ] },
}},
// But "sets" are not considered ordered, so sort them
{ "$unwind": "$orders" },
{ "$sort": { "orders.year": 1, "orders.month": 1 } },
{ "$group": {
"_id": "$_id",
"orders": { "$push": "$orders" },
"deliveries": { "$first": "$deliveries" }
}},
{ "$unwind": "$deliveries" },
{ "$sort": { "deliveries.year": 1, "deliveries.month": 1 } },
{ "$group": {
"_id": "$_id",
"orders": { "$first": "$orders" },
"deliveries": { "$push": "$deliveries" }
}}
)
Just doing that a little differently for pre 2.6 versions:
db.transactions.aggregate([
// Project an additional array, stands for "order", "delivery"
{ "$project": {
"_id": 0,
"customer": 1,
"order_date": 1,
"delivery_date": 1,
"qty": 1,
"type": { "$cond": [ 1, ["o","d"], 0 ] }
}},
// Unwind that array, creates two documents by "type"
{ "$unwind": "$type" },
// Group by "customer", "type" and date
{ "$group": {
"_id": {
"customer": "$customer",
"type": "$type",
"year": {
"$year": {
"$cond": [
{ "$eq": [ "$type", "o" ] },
"$order_date",
"$delivery_date"
]
}
},
"month": {
"$month": {
"$cond": [
{ "$eq": [ "$type", "o" ] },
"$order_date",
"$delivery_date"
]
}
}
},
"qty": { "$sum": "$qty" }
}},
// Group on the "customer" selecting which array to add to
{ "$group": {
"_id": "$_id.customer",
"orders": {
"$push": {
"$cond": [
{ "$eq": [ "$_id.type", "o" ] },
{
"year": "$_id.year",
"month": "$_id.month",
"qty": "$qty"
},
false
]
}
},
"deliveries": {
"$push": {
"$cond": [
{ "$eq": [ "$_id.type", "d" ] },
{
"year": "$_id.year",
"month": "$_id.month",
"qty": "$qty"
},
false
]
}
}
}},
// Filter `false` and sort on date
{ "$unwind": "$orders" },
{ "$match": { "orders": { "$ne": false } } },
{ "$sort": { "orders.year": 1, "orders.month": 1 } },
{ "$group": {
"_id": "$_id",
"orders": { "$push": "$orders" },
"deliveries": { "$first": "$deliveries" }
}},
{ "$unwind": "$deliveries" },
{ "$match": { "deliveries": { "$ne": false } } },
{ "$sort": { "deliveries.year": 1, "deliveries.month": 1 } },
{ "$group": {
"_id": "$_id",
"orders": { "$first": "$orders" },
"deliveries": { "$push": "$deliveries" }
}}
])
Basically to sum up the approach here, what you are doing is duplicating every document and assigning a "type" representing "order" or "delivery". Then as you group by "customer" and "date" and "type" you conditionally decide which "date" to choose based on the current type, and just sum up the "qty" under that key.
Since the result is an array of "orders" and "deliveries" per customer you then conditionally $push
to that array either the document values or false
depending on what the current "type" of the document is to each array.
Finally, since these arrays now contain values for false
as well as the required documents you filter out those values and make sure your arrays are in the correct "date" order if you actually require that.
Yes the listings have more than two $group
stages, the heavy lifting is actually done in two groupings with the others just there for array manipulation if you require it, but it gives you exact and ordered results.
So that is likely not the first approach you might have thought of, but shows some of the interesting transformation ideas you can use with various aggregation operators in order to solve the problem. Which this does :)
I was experiencing a similar issue wherein I needed to get my result categorised in multiple groups and looking at all those answer made my head spin. After researching a lot, I found the exact thing that i was looking for.
MongoDB introduced a new command in version 3.4 called $facet, which makes it very easy to include multiple groups in a single command. Have a look at their docs:
$facet (aggregation)
I was going to explain it all in text here, but I think their doc is far more clear and beautifully written with good examples.
Hope it helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With