mongodb query to find the min price in array of objects - mongodb

My documents:
[{
"title": "lenovo x-100",
"brand": "lenovo",
"category": "laptops",
"variant": [{
"price": 30000,
"RAM": "4GB",
"storage": "256GB",
"screen": "full hd",
"chip": "i3"
}, {
"price": 35000,
"RAM": "8GB",
"storage": "512GB",
"screen": "full hd",
"chip": "i5"
}, {
"price": 40000,
"RAM": "12GB",
"storage": "2TB",
"screen": "uhd",
"chip": "i7"
}],
"salesCount": 32,
"buysCount": 35,
"viewsCount": 60
},
{
"title": "samsung12",
"brand": "lenovo",
"category": "mobile phones",
"variant": [{
"price": 11000,
"RAM": "4GB",
"ROM": "32GB"
}, {
"price": 16000,
"RAM": "6GB",
"ROM": "64GB"
}, {
"price": 21000,
"RAM": "8GB",
"ROM": "128GB"
}],
"salesCount": 48,
"buysCount": 39,
"viewsCount": 74
}
Expected output
{
_id:"lenovo",
minPrice:1100
}
I have tried this method of aggregation
[{
$match: {
brand: 'lenovo'
}
}, {
$group: {
_id: '$brand',
prices: {
$min: '$variant.price'
}
}
}, {
$unwind: {
path: '$prices'
}
}, {
$group: {
_id: '$_id',
minPrice: {
$min: '$prices'
}
}
}]
I want to find the minimum price based on the brand, this query is returning the expected output but is there any better way to get the expected outcome because using $unwind operator in quite expensive in the sense it may take longer execution time, hoping for positive response.Thanks in advance.

You can use $reduce to replace the second $group stage.
$match
$group - Push variant.price into new array and results nested array of array.
$project:
3.1. $reduce - Use to flatten the nested array from the result 2 by $concat the arrays into one.
3.2. $min - Select min value from the result 3.1.
db.collection.aggregate([
{
$match: {
brand: "lenovo"
}
},
{
$group: {
_id: "$brand",
prices: {
$push: "$variant.price"
}
}
},
{
$project: {
_id: 1,
minPrice: {
$min: {
"$reduce": {
"input": "$prices",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
}
}
}
}
])
Sample Mongo Playground

Related

MongoDB - Sum the field in an array

How can I get all the sum of fields in an array in Mongoose?
I want to sum up all the amounts in the payments array.
DB:
[
{
"_id": 0,
"name": "shoe",
"payments": [
{
"type": "a",
"amount": 10
},
{
"type": "b",
"amount": 15
},
{
"type": "a",
"amount": 15
},
]
},
{
"_id": 0,
"name": "shirt",
"payments": [
{
"type": "a",
"amount": 5
},
{
"type": "b",
"amount": 20
},
]
}
]
Expected result:
{
"amountSum": 65
}
There is a shorter and most likely faster solution:
db.collection.aggregate([
{
$group: {
_id: null,
amountSum: { $sum: { $sum: "$payments.amount" } }
}
}
])
$group - Group all documents.
1.1. $sum - Sum the value returned from 1.1.1 for the amountSum field.
1.1.1. $reduce - As payments is an array of objects, sum all the amount for the elements and transform the result from the array to number.
db.collection.aggregate([
{
$group: {
_id: null,
amountSum: {
$sum: {
$reduce: {
input: "$payments",
initialValue: 0,
in: {
$sum: [
"$$value",
"$$this.amount"
]
}
}
}
}
}
}
])
Demo # Mongo Playground

mongodb set/update aggregated unwound fields

I have a document from mongo database looking like this:
{
"_id": "00000001",
"category": "Weather",
"city": "Salt Lake City",
"date": {
"$date": {
"$numberLong": "1663236000000"
}
},
"logs": {
"2022-09-14 12:00:00": {
"temp": 55,
"humidity": 25
},
"2022-09-14 14:00:00": {
"temp": 65,
"humidity": 35
}
}
}
I am trying to query it and have it look like this:
{
"_id": "00000001",
"category": "Weather",
"city": "Salt Lake City",
"date": {
"$date": {
"$numberLong": "1663236000000"
}
},
"2022-09-14 12:00:00": "55, 25",
"2022-09-14 14:00:00": "65, 35"
}
Currently my application query looks like:
collection.aggregate(
[{
$match: {
_id: {
$exists: true
}
}
},
{
$unwind: "$logs"
},
{
$addFields: {
"series._id": "$_id",
"series.category": "$category",
"series.city": "$city",
"series.date": "$date",
}
},
{
$replaceRoot: {
newRoot: "$logs"
},
}
])
which results in:
{
"_id": "00000001",
"category": "Weather",
"city": "Salt Lake City",
"date": {
"$date": {
"$numberLong": "1663236000000"
}
},
"2022-09-14 12:00:00": {
"temp": 55,
"humidity": 25
},
"2022-09-14 14:00:00": {
"temp": 65,
"humidity": 35
}
}
My problem is that the logs will add a new field every n hours, so the field names will be dynamic. I need to set/update the values for the unwound fields from objects to a string representation. How can I set/update field values for fields generated through $unwind aggregation like the example?
When field names are dynamic, one option is to use $objectToArray:
db.collection.aggregate([
{$match: {_id: {$exists: true}}},
{$set: {logs: {$objectToArray: "$logs"}}},
{$set: {logs: {
$map: {
input: "$logs",
in: {
k: "$$this.k",
v: {$concat: [
{$toString: "$$this.v.temp"},
", ",
{$toString: "$$this.v.humidity"}
]
}
}
}
}
}
},
{$set: {logs: {$arrayToObject: "$logs"}}}
])
See how it works on the playground example
BTW, $unwind is for arrays, not for objects, hence the comment by #CharchitKapoor.
Building off of #nimrod serok's answer, still needed to flatten the logs field in my case. Used mergeObjects to flatten the field into the root document, and then used unset to remove the original field. This probably isn't the best way to do this but it is working for me. Thanks
[{$match: {_id: {$exists: true}}},
{$set: {logs: {$objectToArray: "$logs"}}},
{$set: {logs: {
$map: {
input: "$logs",
in: {
k: "$$this.k",
v: {$concat: [
{$toString: "$$this.v.temp"},
", ",
{$toString: "$$this.v.humidity"}
]
}
}
}
}
}
},
{$replaceRoot: { newRoot: { $mergeObjects: ["$$ROOT", {$arrayToObject: "$logs"}] } } },
{$unset: "logs"}
]

Get min value from array of object using aggregate and lookup mongodb

I have two collections properties and property_prices and the relation is one property many prices. So I am trying to join them and then find min value from property_prices.monthly_unit_price.unit_price. So I could get the Properties with their prices and min unit_price value from entire property pricing.
Property Collection
{
"_id": "1",
"status": "Approved",
"name": "My Property Lake"
}
Property Price Collection where monthly_unit_price have objects from Jan - Dec
{
"property_prices": [
{
"property_id": "1",
"block_id": "ABC",
"monthly_unit_price": [{ "month": "Jan", "unit_price": 100 }, { "month": "Dec", "unit_price": "1200" }],
},
{
"property_id": "1",
"block_id": "DEF",
"monthly_unit_price": [{ "month": "Jan", "unit_price": "200" }, { "month": "Dec", "unit_price": "2400" }],
}
]
}
Basically I want to get the min value from property_prices unit_price for property_id 1
So I tried using aggregate and lookup but I cant get the min value for entire property from property_prices.
Here is what I tried
await Property.aggregate([
{
$lookup: {
from: 'property_prices',
as: 'property_prices',
let: { property_id: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$property_id', '$$property_id'] },
{ $eq: ['$status', 'Completed'] },
]
}
}
},
]
},
},
{
$unwind: "$property_prices"
},
{
$group: {
_id: '$property_prices.property_id',
minInvestment: { "$min": "$property_prices.monthly_unit_price.unit_price" }
}
},
]);
Result I am expecting is
{
"_id": "1",
"status": "Approved",
"name": "My Property Lake",
"property_prices": [
{
"property_id": "1",
"block_id": "ABC",
"monthly_unit_price": [{ "month": "Jan", "unit_price": 100 }, { "month": "Dec", "unit_price": "1200" }],
},
{
"property_id": "1",
"block_id": "DEF",
"monthly_unit_price": [{ "month": "Jan", "unit_price": "200" }, { "month": "Dec", "unit_price": "2400" }],
}
],
"minInvestment":100
}
You are on the right track, you just need to "massage" the document structure a little bit more due to the fact it's a nested array. here is a quick example of doing so using the $map and $reduce operators.
Notice I also had to cast the values to number type using $toInt, I recommend these sort of things to be handled at update/insertion time instead.
db.properties.aggregate([
{
$lookup: {
from: "property_prices",
as: "property_prices",
let: {
property_id: "$_id"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{
$eq: [
"$property_id",
"$$property_id"
]
},
{
$eq: [
"$status",
"Completed"
]
}
]
}
}
}
]
}
},
{
$addFields: {
minInvestment: {
$min: {
$reduce: {
input: {
$map: {
input: "$property_prices",
as: "property",
in: {
$map: {
input: "$$property.monthly_unit_price",
as: "price",
in: {
$toInt: "$$price.unit_price"
}
}
}
}
},
initialValue: [],
in: {
"$concatArrays": [
"$$value",
"$$this"
]
}
}
}
}
}
}
])
Mongo Playground

Mongodb aggregate addField that contains the current object position in the array query

After the last aggregate pipeline I receive the current array:
[{
"cdrId": "61574b3e58fb1cae1494df2c",
"date": "2021-10-01T17:54:06.057Z",
"intentType": "FALLBACK"
},
{
"cdrId": "61574b3e58fb1cae1494df2c",
"date": "2021-10-01T17:54:06.057Z",
"intentType": "FAQ"
},
{
"cdrId": "61570b37522aba5e2f205356",
"date": "2021-10-01T13:20:55.601Z",
"intentType": "TRANS/DISAM"
},
{
"cdrId": "61570b37522aba5e2f205356",
"date": "2021-10-01T13:20:55.601Z",
"intentType": "FAQ"
}]
I'm looking to add a index field showing the current position of the object in the array. The output is going to be something like this:
[{
"index": 0,
"cdrId": "61574b3e58fb1cae1494df2c",
"date": "2021-10-01T17:54:06.057Z",
"intentType": "FALLBACK"
},
{
"index": 1,
"cdrId": "61574b3e58fb1cae1494df2c",
"date": "2021-10-01T17:54:06.057Z",
"intentType": "FAQ"
},
{
"index": 2,
"cdrId": "61570b37522aba5e2f205356",
"date": "2021-10-01T13:20:55.601Z",
"intentType": "TRANS/DISAM"
},
{
"index": 3,
"cdrId": "61570b37522aba5e2f205356",
"date": "2021-10-01T13:20:55.601Z",
"intentType": "FAQ"
}]
I will use this value if the next pipe sort this array. So I have it's original position before this sorting.
Is there a way that I can do this with aggregate? I'm using MongoDB 4.2.
Try this one:
db.collection.aggregate([
// {$sort: {...} },
{
$group: {
_id: null,
data: { $push: "$$ROOT" }
}
},
{
$unwind: {
path: "$data",
includeArrayIndex: "index"
}
},
{
$replaceRoot: {
newRoot: { $mergeObjects: [ "$data", {index: "$index"} ] }
}
}
])
Mongo Playground

MongoDb aggregation with arrays inside an array possible

I am struggling to find some examples of using the mongo aggregation framework to process documents which has an array of items where each item also has an array of other obejects (array containing an array)
In the example document below what I would really like is an example that sums the itemValue in the results array of all cases in the document and accross the collection where the result.decision was 'accepted'and group by the document locationCode
However, even an example that found all documents where the result.decision was 'accepted' to show or that summmed the itemValue for the same would help
Many thanks
{
"_id": "333212",
"data": {
"locationCode": "UK-555-5566",
"mode": "retail",
"caseHandler": "A N Other",
"cases": [{
"caseId": "CSE525666",
"items": [{
"id": "333212-CSE525666-1",
"type": "hardware",
"subType": "print cartridge",
"targetDate": "2020-06-15",
"itemDetail": {
"description": "acme print cartridge",
"quantity": 2,
"weight": "1.5"
},
"result": {
"decision": "rejected",
"decisionDate": "2019-02-02"
},
"isPriority": true
},
{
"id": "333212-CSE525666-2",
"type": "Stationery",
"subType": "other",
"targetDate": "2020-06-15",
"itemDetail": {
"description": "staples box",
"quantity": 3,
"weight": "1.66"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-03-03",
"itemValue": "23.01"
},
"isPriority": true
}
]
},
{
"caseId": "CSE885655",
"items": [{
"id": "333212-CSE885655-1",
"type": "marine goods",
"subType": "fish food",
"targetDate": "2020-06-04",
"itemDetail": {
"description": "fish bait",
"quantity": 5,
"weight": "0.65"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-03-02"
},
"isPriority": false
},
{
"id": "333212-CSE885655-4",
"type": "tobacco products",
"subType": "cigarettes",
"deadlineDate": "2020-06-15",
"itemDetail": {
"description": "rolling tobbaco",
"quantity": 42,
"weight": "2.25"
},
"result": {
"decision": "accepted",
"decisionDate": "2020-02-02",
"itemValue": "48.15"
},
"isPriority": true
}
]
}
]
},
"state": "open"
}
You're probably looking for $unwind. It takes an array within a document and creates a separate document for each array member.
{ foos: [1, 2] } -> { foos: 1 }, { foos: 2}
With that you can create a flat document structure and match & group as normal.
db.collection.aggregate([
{
$unwind: "$data.cases"
},
{
$unwind: "$data.cases.items"
},
{
$match: {
"data.cases.items.result.decision": "accepted"
}
},
{
$group: {
_id: "$data.locationCode",
value: {
$sum: {
$toDecimal: "$data.cases.items.result.itemValue"
}
}
}
},
{
$project: {
_id: 0,
locationCode: "$_id",
value: "$value"
}
}
])
https://mongoplayground.net/p/Xr2WfFyPZS3
Alternative solution...
We group by data.locationCode and sum all items with this condition:
cases[*].items[*].result.decision" == "accepted"
db.collection.aggregate([
{
$group: {
_id: "$data.locationCode",
itemValue: {
$sum: {
$reduce: {
input: "$data.cases",
initialValue: 0,
in: {
$sum: {
$concatArrays: [
[ "$$value" ],
{
$map: {
input: {
$filter: {
input: "$$this.items",
as: "f",
cond: {
$eq: [ "$$f.result.decision", "accepted" ]
}
}
},
as: "item",
in: {
$toDouble: {
$ifNull: [ "$$item.result.itemValue", 0 ]
}
}
}
}
]
}
}
}
}
}
}
}
])
MongoPlayground