mongodb multiple groups in one result

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.