Mongodb- group an array by key - mongodb

I have an array field (contains objects) in multiple documents, I want to merge the arrays into one array and group the array by object key. I have manage to group the array but I dont know how to group the data. See the code I tried below
const test = await salesModel.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: 0,
data: { $addToSet: '$items' }
},
}
])
Result of the query:
{
_id: 0,
data: [
{
_id: 61435b3c0f773abaf77a367e,
price: 3000,
type: 'service',
sellerId: 61307abca667678553be81cb,
},
{
_id: 613115808330be818abaa613,
price: 788,
type: 'product',
sellerId: 61307abca667678553be81cb,
},
{
_id: 61307c1ea667676078be81cc,
price: 1200,
type: 'product',
sellerId: 61307abca667678553be81cb,
}
]
}
Now I want to group the data array by object key data.sellerId and sum price
Desired Output:
{
data: [
{
sumPrice: 788,
sellerId: 613115808330be818abaa613,
},
{
sumPrice: 1200,
sellerId: 61307abca667678553be81cb,
}
]
}

Extend with the current query and result with:
$unwind: Deconstruct the array field to multiple documents.
$group: Group by data.sellerId to sum ($sum) for data.price.
$group: Group by 0 with $addToSet to combine multiple documents into one document with data.
MongoDB aggregation query
db.collection.aggregate([
{
$unwind: "$data"
},
{
$group: {
_id: {
sellerId: "$data.sellerId"
},
"sumPrice": {
$sum: "$data.price"
}
}
},
{
"$group": {
"_id": 0,
"data": {
$addToSet: {
"sellerId": "$_id.sellerId",
"sumPrice": "$sumPrice"
}
}
}
}
])
Sample Mongo Playground
Output
[
{
"_id": 0,
"data": [
{
"sellerId": ObjectId("61307abca667678553be81cb"),
"sumPrice": 4988
}
]
}
]
If you want to re-write the query, here are the query with sample input.
Input
[
{
items: [
{
_id: ObjectId("61435b3c0f773abaf77a367e"),
price: 3000,
type: "service",
sellerId: ObjectId("61307abca667678553be81cb"),
},
{
_id: ObjectId("613115808330be818abaa613"),
price: 788,
type: "product",
sellerId: ObjectId("61307abca667678553be81cb"),
},
{
_id: ObjectId("61307c1ea667676078be81cc"),
price: 1200,
type: "product",
sellerId: ObjectId("61307abca667678553be81cb"),
}
]
}
]
Mongo aggregation query
db.collection.aggregate([
{
$unwind: "$items"
},
{
$group: {
_id: {
sellerId: "$items.sellerId"
},
"sumPrice": {
$sum: "$items.price"
}
}
},
{
"$group": {
"_id": 0,
"data": {
$addToSet: {
"sellerId": "$_id.sellerId",
"sumPrice": "$sumPrice"
}
}
}
}
])
Sample 2 on Mongo Playground
Output
[
{
"_id": 0,
"data": [
{
"sellerId": ObjectId("61307abca667678553be81cb"),
"sumPrice": 4988
}
]
}
]

Related

Combine two mongo aggregate query results into one

I have two mongo aggregate pipelines that output results. Now I want combine these two pipelines to have a singular output.
Please find below sample collection.
[
{
_id: "ddfdfdfdggfgfgsg",
rate: "3323",
quantity_packs: "343",
shop_name: "Whole Foods",
sku: "20"
manufacturer_name: "Unilever"
},
{
_id: "ddfdfdfsdsds",
rate: "434",
quantity_packs: "453",
shop_name: "Carrefour",
sku: "200"
manufacturer_name: "Unilever"
},
{
_id: "dfdfdgcvgfgfvvv",
rate: "343",
quantity_packs: "23",
shop_name: "Target",
manufacturer_name: "Beirsdorf"
sku: "34"
}
]
Please find below my queries.
First Query
db.collection.aggregate([
{
$match: {
manufacturer_name: {
$in: [ "unilever" ]
}
}
},
{
$group: {
_id: {
"Shop Name": "$shop_name"
},
"total_sku": {
"$addToSet": "$sku"
},
"annual_cost": {
$sum: {
$cond: [
{
$eq: ["$manufacturer_name", "unilever"]
},
{
"$toDouble": "$rate"
},
0
]
}
},
"annual_qty": {
$sum: {
"$toDouble": "$annual_qty"
}
}
}
},
{
$project: {
"sku count": {
"$size": "$total_sku"
},
"Annual Cost WO GST": {
$multiply: [ "$annual_cost", "$annual_qty" ]
},
}
},
])
Result of First Query
[
{
_id: { 'Hospital Name': '7AM mart' },
'sku count': 29,
'Annual Cost WO GST': 79968887.67999999
},
{
_id: { 'Shop Name': 'Apex' },
'sku count': 20,
'Annual Cost WO GST': 1779192666.96
}
]
Second Query
db.collection.aggregate([
{
$match: {
$expr: {
$ne: ["$manufacturer_name", "unilever"]
}
}
},
{
$group: {
_id: {
"Shop Name": "$shop_name"
},
"annual_cost_wo_gst_wo_manu": {
$sum: {
"$toDouble": "$rate"
}
},
"annual_qty": {
$sum: {
"$toDouble": "$annual_qty"
}
}
}
},
{
$project: {
"Ann Cost For Other Manufacturers": {
$multiply: ["$annual_cost_wo_gst_wo_manu", "$annual_qty"]
},
}
}
])
Result of Second Query
[
{
_id: { 'Hospital Name': 'Apex' },
'Ann Cost For Other Manufacturers': 25246715130525.273
},
{
_id: { 'Hospital Name': '7AM Mart' },
'Ann Cost For Other Manufacturers': 1347701834351.495
}
]
As mentioned above, I somehow want to combine to results by correctly mapping the items.
Intended Result
[
{
_id: { 'Hospital Name': '7AM mart' },
'sku count': 29,
'Annual Cost WO GST': 79968887.67999999
'Ann Cost For Other Manufacturers': 1347701834351.495
},
{
_id: { 'Shop Name': 'Apex' },
'sku count': 20,
'Annual Cost WO GST': 1779192666.96
'Ann Cost For Other Manufacturers': 25246715130525.273
}
]
Your 2 queries do not quite produce your stated outputs. Nevertheless, you could first perform uncorrelated $lookup to perform your second query, storing the result of your secondary query in a field/object. Then you can continue your first query. Finally extract the result of secondary query from the previously stored field/object.
Here is a Mongo playground with some modifications to your original examples for your reference.

MongoDB get size of unwinded array

I'm trying to return size of 'orders' and sum of 'item' values for each 'order' for each order from documents like the example document:
orders: [
{
order_id: 1,
items: [
{
item_id: 1,
value:100
},
{
item_id: 2,
value:200
}
]
},
{
order_id: 2,
items: [
{
item_id: 3,
value:300
},
{
item_id: 4,
value:400
}
]
}
]
I'm using following aggregation to return them, everything works fine except I can't get size of 'orders' array because after unwind, 'orders' array is turned into an object and I can't call $size on it since it is an object now.
db.users.aggregate([
{
$unwind: "$orders"
},
{
$project: {
_id: 0,
total_values: {
$reduce: {
input: "$orders.items",
initialValue: 0,
in: { $add: ["$$value", "$$this.value"] }
}
},
order_count: {$size: '$orders'}, //I get 'The argument to $size must be an array, but was of type: object' error
}
},
])
the result I expected is:
{order_count:2, total_values:1000} //For example document
{order_count:3, total_values:1500}
{order_count:5, total_values:2500}
I found a way to get the results that I wanted. Here is the code
db.users.aggregate([
{
$project: {
_id: 1, orders: 1, order_count: { $size: '$orders' }
}
},
{ $unwind: '$orders' },
{
$project: {
_id: '$_id', items: '$orders.items', order_count: '$order_count'
}
},
{ $unwind: '$items' },
{
$project: {
_id: '$_id', sum: { $sum: '$items.value' }, order_count: '$order_count'
}
},
{
$group: {
_id: { _id: '$_id', order_count: '$order_count' }, total_values: { $sum: '$sum' }
}
},
])
output:
{ _id: { _id: ObjectId("5dffc33002ef525620ef09f1"), order_count: 2 }, total_values: 1000 }
{ _id: { _id: ObjectId("5dffc33002ef525620ef09f2"), order_count: 3 }, total_values: 1500 }

How to $count and $group within MongoDB aggregation?

I would like to count the status and group them by country.
Data:
[
{ id: 100, status: 'ordered', country: 'US', items: [] },
{ id: 101, status: 'ordered', country: 'UK', items: [] },
{ id: 102, status: 'shipped', country: 'UK', items: [] },
]
Desired aggregation outcome:
[
{ _id: 'US', status: { ordered: 1} },
{ _id: 'UK', status: { ordered: 1, shipped: 1 } }
]
I can $count and $group, but I am not sure how to put this together. Any hint is appreciated.
Thanks,
bluepuama
$group by country and status, and count total
$group by only country and construct array of status and count in key-value format
$set to update status field to object using $arrayToObject
db.collection.aggregate([
{
$group: {
_id: { country: "$country", status: "$status" },
count: { $sum: 1 }
}
},
{
$group: {
_id: "$_id.country",
status: { $push: { k: "$_id.status", v: "$count" } }
}
},
{ $set: { status: { $arrayToObject: "$status" } } }
])
Playground
You can do it with a single $group stage like so:
db.collection.aggregate([
{
$group: {
_id: "$country",
"shipped": {
$sum: {
$cond: [
{
$eq: [
"$status",
"ordered"
]
},
0,
1
]
}
},
"ordered": {
$sum: {
$cond: [
{
$eq: [
"$status",
"shipped"
]
},
0,
1
]
}
}
}
},
{
$project: {
_id: 1,
status: {
shipped: "$shipped",
ordered: "$ordered"
}
}
}
])
Mongo Playground

Mongoose subquery

I have a collection that looks like below:
[
{
"orderNum": "100",
"createdTime": ISODate("2020-12-01T21:00:00.000Z"),
"amount": 100,
"memo": "100memo",
"list": [
1
]
},
{
"orderNum": "200",
"createdTime": ISODate("2020-12-01T21:01:00.000Z"),
"amount": 200,
"memo": "200memo",
"list": [
1,
2
]
},
{
"orderNum": "300",
"createdTime": ISODate("2020-12-01T21:02:00.000Z"),
"amount": 300,
"memo": "300memo"
},
{
"orderNum": "400",
"createdTime": ISODate("2020-12-01T21:03:00.000Z"),
"amount": 400,
"memo": "400memo"
},
]
and I'm trying to get the total amount of orders that were created before order# 300 (so order#100 and #200, total amount is 300).
Does anyone know how to get it via Mongoose?
You can use this one:
db.collection.aggregate([
{ $sort: { orderNum: 1 } }, // by default the order of documents in a collection is undetermined
{ $group: { _id: null, data: { $push: "$$ROOT" } } }, // put all documents into one document
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } }, // cut desired elementes from array
{ $unwind: "$data" }, // transform back to documents
{ $replaceRoot: { newRoot: "$data" } },
{ $group: { _id: null, total_amount: { $sum: "$amount" } } } // make summary
])
Actually it is not needed to $unwind and $group, so the shortcut would be this:
db.collection.aggregate([
{ $sort: { orderNum: 1 } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $project: { total_amount: { $sum: "$data.amount" } } }
])
But the answer from #turivishal is even better.
Update for additional field
{
$set: {
data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] },
memo: { $arrayElemAt: [ "$data.memo", { $indexOfArray: ["$data.orderNum", "300"] } ] }
}
}
or
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $set: { memo: { $last: { "$data.memo" } } },
$match orderNum less than 300
$group by null and get totalAmount using $sum of amount
YourSchemaModel.aggregate([
{ $match: { orderNum: { $lt: "300" } } },
{
$group: {
_id: null,
totalAmount: { $sum: "$amount" }
}
}
])
Playground

Mongoose aggregate the property of subdocument and display the result

I have a document with a subdocument (not referenced). I want to apply the aggregation on the field of the subdocument.
Schema
const MFileSchema = new Schema({
path: String,
malwareNames: [String],
title: String,
severity: String // i want to aggregate bases on this field
});
const ScanSchema = new Schema({
agent: { type: Schema.Types.ObjectId, ref: "Agent" },
completedAt: Date,
startedAt: { type: Date, default: Date.now() },
mFiles: [MFileSchema] // array of malicious files schema
});
Model
let Scan = model("Scan", ScanSchema);
Task
Find the sum of severity in all scan documents of particular agents.
// agents is an array Agents (the schema is not important to show, consider the _id)
The Aggregation Query I am using
let c = await Scan.aggregate([
{ $match: { agent: agents } },
{ $project: { "mFiles.severity": true } },
{ $group: { _id: "$mFiles.severity", count: { $sum: 1 } } }
]);
console.log(c);
Actual Output
[]
Expected Output
// The value of count in this question is arbitrary
[
{ _id: "Critical", count: 30 },
{ _id: "Moderate", count: 33 },
{ _id: "Clean", count: 500 }
]
PS: Also I would appreciate if you could suggest me the best resources to learn MongoDB aggregations
You need to use $in query operator in the $match stage, and add $unwind stage before $group stage.
db.collection.aggregate([
{
$match: {
agent: {
$in: [
"5e2c98fc3d785252ce5b5693",
"5e2c98fc3d785252ce5b5694"
]
}
}
},
{
$project: {
"mFiles.severity": true
}
},
{
$unwind: "$mFiles"
},
{
$group: {
_id: "$mFiles.severity",
count: {
$sum: 1
}
}
}
])
Playground
Sample data:
[
{
"agent": "5e2c98fc3d785252ce5b5693",
"mFiles": [
{
"title": "t1",
"severity": "Critical"
},
{
"title": "t2",
"severity": "Critical"
},
{
"title": "t3",
"severity": "Moderate"
},
{
"title": "t4",
"severity": "Clean"
}
]
},
{
"agent": "5e2c98fc3d785252ce5b5694",
"mFiles": [
{
"title": "t5",
"severity": "Critical"
},
{
"title": "t6",
"severity": "Critical"
},
{
"title": "t7",
"severity": "Moderate"
}
]
}
]
Output:
[
{
"_id": "Moderate",
"count": 2
},
{
"_id": "Critical",
"count": 4
},
{
"_id": "Clean",
"count": 1
}
]
For mongoose integration:
//agents must be an array of objectIds like this
// [ObjectId("5e2c98fc3d785252ce5b5693"), ObjectId("5e2c98fc3d785252ce5b5694")]
//or ["5e2c98fc3d785252ce5b5693","5e2c98fc3d785252ce5b5694"]
const ObjectId = require("mongoose").Types.ObjectId;
let c = await Scan.aggregate([
{
$match: {
agent: {
$in: agents
}
}
},
{
$project: {
"mFiles.severity": true
}
},
{
$unwind: "$mFiles"
},
{
$group: {
_id: "$mFiles.severity",
count: {
$sum: 1
}
}
}
]);
Best place for learning mongodb aggregation is the official docs.