MongoDB $group will not allow me to $project extra fields - mongodb

I almost got this one working, but I simply cannot figure out why the $project part does not work for normal fields....
This is "invoice" table:
{
"_id" : "AS6D0",
"invoiceNumber" : 23,
"bookingId" : "AS6D0",
"createDate" : 1490369414,
"dueDate" : 1490369414,
"invoiceLines" : [
{
"lineText" : "Rent Price",
"amountPcs" : "8 x 7500",
"amountTotal" : 60000
},
{
"lineText" : "Discount(TIKO10)",
"amountPcs" : "10%",
"amountTotal" : -10000
},
{
"lineText" : "Final cleaning",
"amountPcs" : "1 x 5000",
"amountTotal" : 5000
},
{
"lineText" : "Reservation fee paid already",
"amountPcs" : "1 x -20000",
"amountTotal" : -20000
}
],
"managerId" : "4M4KE"
}
And this is my query
db.getCollection('invoice').aggregate([
{
$match: {
bookingId: "AS6D0"
}
},
{
$unwind: "$invoiceLines"
},
{
$group: {
_id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"}
}
},
{
$project:{
"_id" : 0,
"invoiceNumber" : 1,
"dueDate" : 1,
"sum" : 1
}
}
])
I get the _id and the sum, but it wont show invoiceNumber and dueDate

You could use a trick like this :
db.getCollection('invoice').aggregate([
{ $match: { } },
{ $unwind: "$invoiceLines" },
{ $group: { _id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"},
invoiceNumber: { $addToSet: "$invoiceNumber" },
dueDate: { $addToSet: "$dueDate" } } }
]);

Thanks to Mateo, this is what I ended up with:
(I do the unwind on the fields to avoid single value arrays)
Update : You don't have to $addToSet to reduce the fields into single value arrays and $unwind. Use $first instead.
db.getCollection('invoice').aggregate([
{
$match: {
bookingId: "AS6D0"
}
},
{
$unwind: "$invoiceLines"
},
{
$group: {
_id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"},
invoiceNumber: { $first: "$invoiceNumber" },
dueDate: { $first: "$dueDate" }
}
},
{
$project:{
"_id" : 0,
"invoiceNumber" : 1,
"dueDate" : 1,
"sum" : 1
}
}
])

Related

How to calculate profit using aggregations from two collections in mongodb?

I have two collections, orders and producttypes
ProductTypes:
{
"_id" : 609d79de5909592f2635c64e,
"name" : "T-Shirt",
"subType" : "Round Neck",
"__v" : 0,
"size" : "XXL",
"sellingPrice" : 320,
"createdAt" : ISODate("2021-05-18T05:22:00.695+0000"),
"actualPrice" : 200,
"updatedAt" : ISODate("2021-05-25T12:11:50.986+0000")
},
{
"_id" : 609d79de5909592f2635c64d,
"name" : "T-Shirt",
"subType" : "V Neck",
"__v" : 0,
"size" : "XXL",
"sellingPrice" : 290,
"createdAt" : ISODate("2021-05-18T05:22:00.695+0000"),
"actualPrice" : 200,
"updatedAt" : ISODate("2021-05-25T12:11:50.986+0000")
}
Orders:
{
"_id" : "60a63e369cf3a806c0209bd8",
"items" : [
{
"type" : "609d79de5909592f2635c64e",
"quantity" : 1,
"sellingPrice" : 320
},
{
"type" : "609d79de5909592f2635c64d",
"quantity" : 2,
"sellingPrice" : 290
}
],
"orderId" : "ORD101",
"from" : "Abc",
"to" : "xyz",
"createdAt" : ISODate("2021-05-20T10:47:18.920+0000"),
"__v" : 0,
"tracking" : "12345678"
}
I want to calculate total profit per order like:
{orderId: "ORD101", createdAt: ISODate("2021-05-18T05:22:00.695+0000"), profit: 300}
I don't know how to join these two collections to calculate the profit.
But I tried something like below in node:
Order.aggregate([{
$unwind: '$items'
}, {
$project: {
orderId:1,
quantity: "$items.quantity",
sellingPrice: {
$multiply: [
{"$ifNull": ["$items.quantity", 0]},
{"$ifNull": ["$items.price", 0]}
]
},
type: '$items.type'
}
}])
.exec(function(err, transactions) {
//console.log(transactions);
ProductType.populate(transactions,{path: 'type', select: 'actualPrice' }, function(err, populatedTransactions) {
//res.json(populatedTransactions);
var items = [];
var totalProfit = 0;
if(populatedTransactions){
populatedTransactions.forEach( order => {
if( order.quantity != undefined && order.sellingPrice != undefined && order.sellingPrice > 0){
let profit = order.sellingPrice - (order.quantity * order.type.actualPrice);
totalProfit = totalProfit + profit;
items.push({ orderId: order.orderId, profit: profit });
}
})
res.status(200).json({data: items, totalProfit: totalProfit});
}
});
});
Is this the right way?
Here am using $unwind on the array then populating with producttypes collection to get an actual price, then am doing calculations to get the profit.
$project to show required fields
$unwind deconstruct the items array
$lookup with productTypes collection
calculate the profit
$arrayElemAt to get first element from item actualPrice result
$subtract sellingPrice by actualPrice
$multiply above result with quantity
$group by order _id and get required fields and sum profit
Order.aggregate([
{
$project: {
orderId: 1,
createdAt: 1,
items: 1
}
},
{ $unwind: "$items" },
{
$lookup: {
from: "productTypes", // replace your actual collection name
localField: "items.type",
foreignField: "_id",
as: "item"
}
},
{
$addFields: {
profit: {
$multiply: [
{
$subtract: [
"$items.sellingPrice",
{ $arrayElemAt: ["$item.actualPrice", 0] }
]
},
"$items.quantity"
]
}
}
},
{
$group: {
_id: "$_id",
orderId: { $first: "$orderId" },
createdAt: { $first: "$createdAt" },
profit: { $sum: "$profit" }
}
}
])
Playground

Filter by nested arrays/objects values (on different levels) and $push by multiple level - MongoDB Aggregate

I have a document with multiple level of embedded subdocument each has some nested array. Using $unwind and sort, do sorting based on day in descending and using push to combine each row records into single array. This Push is working only at one level means it allows only one push. If want to do the same things on the nested level and retains the top level data, got "errmsg" : "Unrecognized expression '$push'".
{
"_id" : ObjectId("5f5638d0ff25e01482432803"),
"name" : "XXXX",
"mobileNo" : 323232323,
"payroll" : [
{
"_id" : ObjectId("5f5638d0ff25e01482432801"),
"month" : "Jan",
"salary" : 18200,
"payrollDetails" : [
{
"day" : "1",
"salary" : 200,
},
{
"day" : "2",
"salary" : 201,
}
]
},
{
"_id" : ObjectId("5f5638d0ff25e01482432802"),
"month" : "Feb",
"salary" : 8300,
"payrollDetails" : [
{
"day" : "1",
"salary" : 300,
},
{
"day" : "2",
"salary" : 400,
}
]
}
],
}
Expected Result:
{
"_id" : ObjectId("5f5638d0ff25e01482432803"),
"name" : "XXXX",
"mobileNo" : 323232323,
"payroll" : [
{
"_id" : ObjectId("5f5638d0ff25e01482432801"),
"month" : "Jan",
"salary" : 18200,
"payrollDetails" : [
{
"day" : "2",
"salary" : 201
},
{
"day" : "1",
"salary" : 200
}
]
},
{
"_id" : ObjectId("5f5638d0ff25e01482432802"),
"month" : "Feb",
"salary" : 8300,
"payrollDetails" : [
{
"day" : "2",
"salary" : 400
},
{
"day" : "1",
"salary" : 300
}
]
}
],
}
Just day will be sorted and remaining things are same
I have tried but it got unrecognized expression '$push'
db.employee.aggregate([
{$unwind: '$payroll'},
{$unwind: '$payroll.payrollDetails'},
{$sort: {'payroll.payrollDetails.day': -1}},
{$group: {_id: '$_id', payroll: {$push: {payrollDetails:{$push:
'$payroll.payrollDetails'} }}}}])
It requires two time $group, you can't use $push operator two times in a field,
$group by main id and payroll id, construct payrollDetails array
$sort by payroll id (you can skip if not required)
$group by main id and construct payroll array
db.employee.aggregate([
{ $unwind: "$payroll" },
{ $unwind: "$payroll.payrollDetails" },
{ $sort: { "payroll.payrollDetails.day": -1 } },
{
$group: {
_id: {
_id: "$_id",
pid: "$payroll._id"
},
name: { $first: "$name" },
mobileNo: { $first: "$mobileNo" },
payrollDetails: { $push: "$payroll.payrollDetails" },
month: { $first: "$payroll.month" },
salary: { $first: "$payroll.salary" }
}
},
{ $sort: { "payroll._id": -1 } },
{
$group: {
_id: "$_id._id",
name: { $first: "$name" },
mobileNo: { $first: "$mobileNo" },
payroll: {
$push: {
_id: "$_id.pid",
month: "$month",
salary: "$salary",
payrollDetails: "$payrollDetails"
}
}
}
}
])
Playground

Need to mark paid installments in an array using mongoDB aggregation

I have a schema named orders which looks like this :
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : NumberInt(10000),
"paidAmount" : NumberInt(4000),
"installments" : [
{
"dueDate" : ISODate("2020-01-01"),
"amount" : NumberInt(2000)
},
{
"dueDate" : ISODate("2020-01-07"),
"amount" : NumberInt(6000)
},
{
"dueDate" : ISODate("2020-01-04"),
"amount" : NumberInt(2000)
}
]
}
I want to write an aggregation function that sorts the installments according to dueDate and mark them paid according to paidAmount. For example for this case the function should return
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : NumberInt(10000),
"paidAmount" : NumberInt(4000),
"installments" : [
{
"dueDate" : ISODate("2020-01-01"),
"amount" : NumberInt(2000),
"paid" : true
},
{
"dueDate" : ISODate("2020-01-04"),
"amount" : NumberInt(2000),
"paid" : true
},
{
"dueDate" : ISODate("2020-01-07"),
"amount" : NumberInt(6000),
"paid" : false
}
]
}
Now I can sort the array using the $unwind and $sort functions like this:
db.orders.aggregate([
{$unwind : "$installments"},
{$sort : {"dueDate" : 1}}
]);
What I am stuck on is how to group the array back so that it gives me the desired result. I can only use aggregation here.
You need to $group installments. But, if you need to put paid field with some logic, it's necessary to add extra pipeline stages.
ASSUMPTION
paidAmount value calculated by ordered installments.[].paid
paidAmount installments.[].paid
4000 <= 2000(t) 2000(t) 6000(f)
4000 <≠ 2000(t) 6000(f) 2000(f)
4000 <≠ 6000(f) 2000(f) 2000(f)
6000 <= 6000(t) 2000(f) 2000(f)
6000 <≠ 1000(t) 6000(f) 1000(f)
6000 <= 1000(t) 4000(t) 1000(t)
EXPLANATION paid:true|false LOGIC
We order installments and create extra tmp field with installments value (for paid field).
For each installments item i, we mark paid:true if paidAmount - sum(amount0 - i) >= 0.
db.orders.aggregate([
{
$unwind: "$installments"
},
{
$sort: {
"installments.dueDate": 1
}
},
{
$group: {
_id: "$_id",
orders: {
$first: "$$ROOT"
},
installments: {
$push: "$installments"
},
tmp: {
$push: "$installments"
}
}
},
{
$unwind: "$installments"
},
{
$addFields: {
"installments.paid": {
$cond: [
{
$gte: [
{
$reduce: {
input: {
$slice: [
"$tmp",
{
$sum: [
{
$indexOfArray: [
"$tmp",
"$installments"
]
},
1
]
}
]
},
initialValue: "$orders.paidAmount",
in: {
$sum: [
{
$multiply: [
"$$this.amount",
-1
]
},
"$$value"
]
}
}
},
0
]
},
true,
false
]
}
}
},
{
$group: {
_id: "$_id",
customerId: {
$first: "$orders.customerId"
},
orderAmount: {
$first: "$orders.orderAmount"
},
paidAmount: {
$first: "$orders.paidAmount"
},
installments: {
$push: "$installments"
}
}
}
])
MongoPlayground
Try this :
db.yourCollectionName.aggregate([{ $unwind: '$installments' },{ $sort: { 'installments.dueDate': 1 } },
{ $addFields: { 'installments.paid': { $cond: [{ $lte: ["$installments.amount", '$paidAmount'] }, true, false] } } },
{ $group: { _id: '$id', data: { $first: '$$ROOT' }, installments: { $push: '$installments' } } },
{ $addFields: { 'data.installments': '$installments' } },
{ $replaceRoot: { newRoot: "$data" } }])
Collection Data :
/* 1 */
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : 10000,
"paidAmount" : 4000,
"installments" : [
{
"dueDate" : ISODate("2020-01-01T21:21:20.202Z"),
"amount" : 2000
},
{
"dueDate" : ISODate("2020-01-07T21:27:20.202Z"),
"amount" : 6000
},
{
"dueDate" : ISODate("2020-01-04T21:24:20.202Z"),
"amount" : 2000
}
]
}
Result :
/* 1 */
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : 10000,
"paidAmount" : 4000,
"installments" : [
{
"dueDate" : ISODate("2020-01-01T21:21:20.202Z"),
"amount" : 2000,
"paid" : true
},
{
"dueDate" : ISODate("2020-01-04T21:24:20.202Z"),
"amount" : 2000,
"paid" : true
},
{
"dueDate" : ISODate("2020-01-07T21:27:20.202Z"),
"amount" : 6000,
"paid" : false
}
]
}
Ref : aggregation-pipeline

How to group by multiple object elements

I have sample data like below
[
{
brand:"iphone",
category:"mobile"
},
{
brand:"iphone",
category:"laptop"
},
{
brand:"lenova",
category:"laptop"
}
]
and expecting result as
[
{
brand:"iphone",
count:2
},
{
brand:"lenova",
count:1
},
{
category:"laptop",
count:2
},
{
category:"mobile",
count:1
}
]
Here I want group by same object with multiple fields and get there count. Can any one please let me how to do that in the mongoose.
I am not familiarised with Mongoose. Just tried in Mongoshell
db.getCollection('test').aggregate([
{
$group:{
_id:"$brand",
brand:{$first:"$brand"},
category:{$first:"$category"}
}
},
{$project:{_id:0}}
])
Possible only by using two queries.
Group By Brand
db.getCollection('pages').aggregate([
{
$group: {_id: "$brand", category: { $push: "$category" }}
},
{
$project : {
_id : 0, brand : "$_id", count : {$size : "$category"}
}
},
{ $unwind: { path: "$category", preserveNullAndEmptyArrays: true } }
])
Result:-
/* 1 */
{
"brand" : "lenova",
"count" : 1
}
/* 2 */
{
"brand" : "iphone",
"count" : 2
}
Group By Category
db.getCollection('pages').aggregate([
{
$group: {
_id: "$category", brand: { $push: "$brand" },
}
},
{
$project : {
_id : 0, category : "$_id", count : {$size : "$brand"}
}
},
{ $unwind: { path: "$brand", preserveNullAndEmptyArrays: true } },
])
Result:-
/* 1 */
{
"category" : "laptop",
"count" : 2
}
/* 2 */
{
"category" : "mobile",
"count" : 1
}
Merge them for the required output.
We can use $facet to run parallel aggregation on data.
The following query can get us the expected output:
db.collection.aggregate([
{
$facet:{
"brand_group":[
{
$group:{
"_id":"$brand",
"brand":{
$first:"$brand"
},
"count":{
$sum:1
}
}
},
{
$project:{
"_id":0
}
}
],
"category_group":[
{
$group:{
"_id":"$category",
"category":{
$first:"$category"
},
"count":{
$sum:1
}
}
},
{
$project:{
"_id":0
}
}
]
}
},
{
$project:{
"array":{
$concatArrays:["$brand_group","$category_group"]
}
}
},
{
$unwind:"$array"
},
{
$replaceRoot:{
"newRoot":"$array"
}
}
]).pretty()
Data set:
{
"_id" : ObjectId("5da5c0d0795c8651a7f508c2"),
"brand" : "iphone",
"category" : "mobile"
}
{
"_id" : ObjectId("5da5c0d0795c8651a7f508c3"),
"brand" : "iphone",
"category" : "laptop"
}
{
"_id" : ObjectId("5da5c0d0795c8651a7f508c4"),
"brand" : "lenova",
"category" : "laptop"
}
Output:
{ "brand" : "lenova", "count" : 1 }
{ "brand" : "iphone", "count" : 2 }
{ "category" : "laptop", "count" : 2 }
{ "category" : "mobile", "count" : 1 }

Is there a way to group results from multiple documents when performing aggregation

I am new to mongo and trying to perform aggregation query to calculate min/max of timestamps for a given document.
Sample documents are below -
{
"_id" : ObjectId("5c9cd93adddca9ebb2b3fcba"),
"frequency" : 5,
"s_id" : "30081993",
"timestamp" : NumberLong(1546300800000),
"date" : ISODate("2019-01-01T00:00:00.000Z"),
"values" : {
"1547439900000" : {
"number_of_values" : 3,
"min_value" : 32.13,
"max_value" : 81.42
},
"1547440200000" : {
"number_of_values" : 3,
"min_value" : 48.08,
"max_value" : 84.52
},
"1547440500000" : {
"number_of_values" : 2,
"min_value" : 27.39,
"max_value" : 94.64
}
}
}
{
"_id" : ObjectId("5c9cd851dddca9ebb2b3f2ac"),
"frequency" : 5,
"s_id" : "27061995",
"timestamp" : NumberLong(1546300800000),
"date" : ISODate("2019-01-01T00:00:00.000Z"),
"values" : {
"1547539900000" : {
"number_of_values" : 31,
"min_value" : 322.13,
"max_value" : 831.42
},
"1547540200000" : {
"number_of_values" : 3,
"min_value" : 418.08,
"max_value" : 8114.52
},
"1547740500000" : {
"number_of_values" : 2,
"min_value" : 207.39,
"max_value" : 940.64
}
}
}
I have come up with the following query which works for a single document.
db.testdb.aggregate([
{
$match: {
"s_id": "30081993",
"frequency": 5,
}
},
{
$project: {
_id: 1,
valuesarray: {
$objectToArray: "$values"
}
}
},
{
$unwind: "$valuesarray"
},
{
$group: {
"_id": "",
"min_timestamp": {
$min: "$valuesarray.k"
},
"max_timestamp": {
$max: "$valuesarray.k"
}
}
}
]);
The output is below
{
"_id" : "",
"min_timestamp" : "1547439900000",
"max_timestamp" : "1547440500000"
}
I want an aggregation query which can calculate the max/min of timestamps but for multiple documents i.e I want to use a $in operator during the $match stage and get min/max of all s_id. Is this possible?
Expected :
{
"_id" : "30081993",
"min_timestamp" : "1547439900000",
"max_timestamp" : "1547440500000"
}
{
"_id" : "27061995",
"min_timestamp" : "1547539900000",
"max_timestamp" : "1547740500000"
}
Yes, only small changes are required to make this work for multiple documents.
In $match stage, specify your $in query:
$match: {
"s_id": { $in : [ "30081993", "27061995" ] },
"frequency": 5,
}
In $project stage, rename s_id to _id, to ensure we keep the s_id associated with each document:
$project: {
_id: "$s_id",
valuesarray: {
$objectToArray: "$values"
}
}
In $group stage, group by _id (originally s_id), to ensure we correctly group the timestamps together before calculating $min/$max:
$group: {
"_id": "$_id",
"min_timestamp": {
$min: "$valuesarray.k"
},
"max_timestamp": {
$max: "$valuesarray.k"
}
}
Whole pipeline:
db.testdb.aggregate([
{
$match: {
"s_id": { $in : [ "30081993", "27061995" ] },
"frequency": 5,
}
},
{
$project: {
_id: "$s_id",
valuesarray: {
$objectToArray: "$values"
}
}
},
{
$unwind: "$valuesarray"
},
{
$group: {
"_id": "$_id",
"min_timestamp": {
$min: "$valuesarray.k"
},
"max_timestamp": {
$max: "$valuesarray.k"
}
}
}
]);