$divide elements of Embedded Documents - MongoDB Aggregation - mongodb

I am trying to create an aggregation MongoDB query.
Structure of data:
{
"object_name": Example,
"values": [ {"name":"value1", "value":1},
{"name":"value2", "value":10},
{"name":"total", "value":105}
}
Goal: Find object names where value1/total > 0.5 and value2/total > 0.25 and total > 100.
The data is structured in this way to provide indexes on the value_name and value fields.
What I tried - aggregate with the following pipelines:
$match: filter documents with total > 100:
$match: { values: { $elemMatch: { value_name: "total", value: {$gte: 100 }
$project: grab only the value_names that we need (there are close to 200 different names)
$project: {
values: {
$filter: {
input: "$values",
as: "value",
cond: { $or: [
{ $eq: [ "$$value.name", "name1"] },
{ $eq: [ "$$value.name", "name2"] },
{ $eq: [ "$$value.name", "total"] },
] }
}
},
name: 1
}
then, { $unwind: "$values" }
And here, I could $group to $divide: name1/total, name2/total however I'm stuck on how to get those values.
I can't simply do stats.value: because it does not know which value I'm referring to. I believe $group can't do $elemMatch to also match the name.
If there are simpler solutions that this, I'd greatly appreciate your input.

Please try this :
We're filtering documents where values array has an object with
name : total & value > 100.
Adding object with name : total
to document.
Leaving only objects that match with criteria
value1/total > 0.5 and value2/total > 0.25 in values array.
If
size of that array is greater than 1, then those two conditions are
met.
Finally projecting only object_name
Query :
db.yourCollectionName.aggregate([{ $match: { values: { $elemMatch: { name: "total", value: { $gte: 100 } } } } },
{
$addFields: {
totalValue: {
$arrayElemAt: [{
$filter: {
input: "$values",
as: "item",
cond: { $eq: ["$$item.name", 'total'] }
}
}, 0]
}
}
},
{
$project: {
values: {
$filter: {
input: "$values",
as: "value",
cond: {
$or: [
{ $cond: [{ $eq: ["$$value.name", "value1"] }, { $gt: [{ $divide: ["$$value.value", '$totalValue.value'] }, 0.5] }, false] },
{ $cond: [{ $eq: ["$$value.name", "value2"] }, { $gt: [{ $divide: ["$$value.value", '$totalValue.value'] }, 0.25] }, false] }
]
}
}
}, object_name: 1
}
}, {
$match: {
$expr: { $gt: [{ $size: "$values" }, 1] }
}
}, { $project: { object_name: 1, _id: 0 } }])
Collection Data :
/* 1 */
{
"_id" : ObjectId("5e20bd94d02e05b694d55fa5"),
"object_name" : "Example",
"values" : [
{
"name" : "value1",
"value" : 1
},
{
"name" : "value2",
"value" : 10
},
{
"name" : "total",
"value" : 105
},
{
"name" : "total1",
"value" : 105
}
]
}
/* 2 */
{
"_id" : ObjectId("5e20bdb1d02e05b694d56490"),
"object_name" : "Example2",
"values" : [
{
"name" : "value1",
"value" : 1
},
{
"name" : "value2",
"value" : 10
},
{
"name" : "total",
"value" : 5
},
{
"name" : "total1",
"value" : 5
}
]
}
/* 3 */
{
"_id" : ObjectId("5e20d1b7d02e05b694d7c57a"),
"object_name" : "Example3",
"values" : [
{
"name" : "value1",
"value" : 100
},
{
"name" : "value2",
"value" : 100
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
/* 4 */
{
"_id" : ObjectId("5e20d1cad02e05b694d7c71c"),
"object_name" : "Example4",
"values" : [
{
"name" : "value1",
"value" : 200
},
{
"name" : "value2",
"value" : 40
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
/* 5 */
{
"_id" : ObjectId("5e20d1e2d02e05b694d7c933"),
"object_name" : "Example5",
"values" : [
{
"name" : "value1",
"value" : 150
},
{
"name" : "value2",
"value" : 100
},
{
"name" : "total",
"value" : 200
},
{
"name" : "total1",
"value" : 205
}
]
}
Result :
/* 1 */
{
"object_name" : "Example5"
}

You may convert your array into object with $arrayToObject operator and add tmp field to have easy access to value1, value2, total values
db.collection.aggregate([
{
$addFields: {
tmp: {
$arrayToObject: {
$map: {
input: "$values",
as: "value",
in: {
k: "$$value.name",
v: "$$value.value"
}
}
}
},
name: 1
}
},
{
$match: {
$expr: {
$and: [
{
$gt: [
{
$divide: [
"$tmp.value1",
"$tmp.total"
]
},
0.5
]
},
{
$gt: [
{
$divide: [
"$tmp.value2",
"$tmp.total"
]
},
0.25
]
},
{
$gt: [
"$tmp.total",
100
]
}
]
}
}
},
{
$project: {
tmp: 0
}
}
])
MongoPlayground

Related

Dynamic fields array in to single object mongo db

I am having mongo collection like below,
{
"_id" : ObjectId("62aeb8301ed12a14a8873df1"),
"Fields" : [
{
"FieldId" : "e8efd0b0-9d10-4584-bb11-5b24f189c03b",
"Value" : [
"test_123"
]
},
{
"FieldId" : "fa6745c2-b259-4a3b-8c6f-19eb78fbbbf5",
"Value" : [
"123"
]
},
{
"FieldId" : "2a1be5d0-8fb6-4b06-a253-55337bfe4bcd",
"Value" : []
},
{
"FieldId" : "eed12747-0923-4290-b09c-5a05107f5609",
"Value" : [
"234234234"
]
},
{
"FieldId" : "fe41d8fb-fa18-4fe5-b047-854403aa4d84",
"Value" : [
"Irrelevan"
]
},
{
"FieldId" : "93e46476-bf2e-44eb-ac73-134403220e9e",
"Value" : [
"test"
]
},
{
"FieldId" : "db434aca-8df3-4caf-bdd7-3ec23252c2c8",
"Value" : [
"2019-06-16T18:30:00.000Z"
]
},
{
"FieldId" : "00df903f-5d59-41c1-a3df-60eeafb77d10",
"Value" : [
"tewt"
]
},
{
"FieldId" : "e97d0386-cd42-6277-1207-e674c3268cec",
"Value" : [
"1"
]
},
{
"FieldId" : "35e55d27-7d2c-467d-8a88-09ad6c9f5631",
"Value" : [
"10"
]
}
]
}
This is all dynamic form fields.
So I want to query and get result like to below object,
{
"_id" : ObjectId("62aeb8301ed12a14a8873df1"),
"e8efd0b0-9d10-4584-bb11-5b24f189c03b": ["test_123"],
"fa6745c2-b259-4a3b-8c6f-19eb78fbbbf5": ["123"],
"2a1be5d0-8fb6-4b06-a253-55337bfe4bcd": [],
"eed12747-0923-4290-b09c-5a05107f5609": ["234234234"],
"fe41d8fb-fa18-4fe5-b047-854403aa4d84": ["Irrelevan"],
"93e46476-bf2e-44eb-ac73-134403220e9e":["test"],
"db434aca-8df3-4caf-bdd7-3ec23252c2c8":["2019-06-16T18:30:00.000Z"],
"00df903f-5d59-41c1-a3df-60eeafb77d10":["1"]
}
I want final output like this combination of fields Fields.FieldID should be key and Fields.Value should be value here.
Please try to help to me to form the object like above.
Thanks in advance!
You can restructure your objects using $arrayToObject, then using that value to as a new root $replaceRoot like so:
db.collection.aggregate([
{
$match: {
// your query here
}
},
{
$project: {
newRoot: {
"$arrayToObject": {
$map: {
input: "$Fields",
in: {
k: "$$this.FieldId",
v: "$$this.Value"
}
}
}
}
}
},
{
"$replaceRoot": {
"newRoot": {
"$mergeObjects": [
"$newRoot",
{
_id: "$_id"
}
]
}
}
}
])
Mongo Playground
I try this and get result like you want
db.collection.aggregate([{
$replaceWith: {
$mergeObjects: [
{
_id: "$_id"
},
{
$arrayToObject: { $zip: {inputs: ["$Fields.FieldId","$Fields.Value"]}}
}
]
}
}])
playground

How to compare two object elements in a mongodb array

{
"customerSchemes": [
{
"name": "A",
"startDate": some date in valid date format
},
{
"name": "B",
"startDate": some date in valid date format.
}
]
}
I am trying to figure out all documents where scheme A started before scheme B.
Please note that the scheme Array is not in ascending order of startDate. Plan B can have an earlier date as compared to plan A.
I believe unwind operator could be of some use here but not sure how to progress with next steps.
aggregate():
$filter to filter name: "A" from customerSchemes
$arrayElemAt to get first element from filtered result from above step
same steps like above for name: "B"
$let to declare variables for "A" in a and "B" in b
in to check condition from above variables if a's startDate is greater than b's startDate then return true otherwise false
$expr expression match with $eq to match above process, if its true then return document
db.collection.aggregate([
{
$match: {
$expr: {
$eq: [
{
$let: {
vars: {
a: {
$arrayElemAt: [
{
$filter: {
input: "$customerSchemes",
cond: { $eq: ["$$this.name", "A"] }
}
},
0
]
},
b: {
$arrayElemAt: [
{
$filter: {
input: "$customerSchemes",
cond: { $eq: ["$$this.name", "B" ] }
}
},
0
]
}
},
in: { $gt: ["$$a.startDate", "$$b.startDate"] }
}
},
true
]
}
}
}
])
Playground
find():
You can use above match stage expression condition in find() query as well without any aggregation pipeline,
Playground
latest support hint: if you are using latest(4.4) MongoDB version then you can use $first instead of $arrayElemAt, see Playground
You could use $unwind array and format the elements for comparison effectively transforming into key value pair. This assumes you only have two array values so I didn't know apply any filtering.
Something like
db.colname.aggregate(
[
{"$unwind":"$customerSchemes"},
{"$group":{
"_id":"$_id",
"data":{"$push":"$$ROOT"},
"fields":{
"$mergeObjects":{
"$arrayToObject":[[["$customerSchemes.name","$customerSchemes.startDate"]]]
}
}
}},
{"$match":{"$expr":{"$lt":["$fields.A","$fields.B"]}}},
{"$project":{"_id":0,"data":1}}
])
Working example here - https://mongoplayground.net/p/mSmAXHm0-o-
Using $reduce
db.colname.aggregate(
[
{"$addFields":{
"fields":{
"$reduce":{
"input":"$customerSchemes",
"initialValue":{},
"in":{
"$mergeObjects":[
{"$arrayToObject":[[["$$this.name","$$this.startDate"]]]},
"$$value"]
}
}
}
}},
{"$match":{"$expr":{"$lt":["$fields.A","$fields.B"]}}},
{"$project":{"fields":0}}
])
Working example here - https://mongoplayground.net/p/WNxbScI9N9b
So the idea is
Sort the customerSchemes array by startDate.
Pick the first item from the sorted list.
Include it only if the customerSchemes.name is A.
Try this query:
db.collection.aggregate([
{ $unwind: "$customerSchemes" },
{
$sort: { "customerSchemes.startDate": 1 }
},
{
$group: {
_id: "$_id",
customerSchemes: { $push: "$customerSchemes" }
}
},
{
$match: {
$expr: {
$eq: [{ $first: "$customerSchemes.name" }, "A"]
}
}
}
]);
Output:
/* 1 createdAt:3/12/2021, 6:40:42 PM*/
{
"_id" : ObjectId("604b685232a8d433d8ede6c4"),
"customerSchemes" : [
{
"name" : "A",
"startDate" : ISODate("2021-03-01T00:00:00.000+05:30")
},
{
"name" : "B",
"startDate" : ISODate("2021-03-02T00:00:00.000+05:30")
}
]
},
/* 2 createdAt:3/12/2021, 6:40:42 PM*/
{
"_id" : ObjectId("604b685232a8d433d8ede6c6"),
"customerSchemes" : [
{
"name" : "A",
"startDate" : ISODate("2021-03-01T00:00:00.000+05:30")
},
{
"name" : "B",
"startDate" : ISODate("2021-03-05T00:00:00.000+05:30")
}
]
}
Test data:
/* 1 createdAt:3/12/2021, 6:40:42 PM*/
{
"_id" : ObjectId("604b685232a8d433d8ede6c4"),
"customerSchemes" : [
{
"name" : "A",
"startDate" : ISODate("2021-03-01T00:00:00.000+05:30")
},
{
"name" : "B",
"startDate" : ISODate("2021-03-02T00:00:00.000+05:30")
}
]
},
/* 2 createdAt:3/12/2021, 6:40:42 PM*/
{
"_id" : ObjectId("604b685232a8d433d8ede6c5"),
"customerSchemes" : [
{
"name" : "A",
"startDate" : ISODate("2021-03-03T00:00:00.000+05:30")
},
{
"name" : "B",
"startDate" : ISODate("2021-03-02T00:00:00.000+05:30")
}
]
},
/* 3 createdAt:3/12/2021, 6:40:42 PM*/
{
"_id" : ObjectId("604b685232a8d433d8ede6c6"),
"customerSchemes" : [
{
"name" : "B",
"startDate" : ISODate("2021-03-05T00:00:00.000+05:30")
},
{
"name" : "A",
"startDate" : ISODate("2021-03-01T00:00:00.000+05:30")
}
]
}

mongodb aggregate to find,count and project unique documnets

Below are the sample collection.
col1:
"_id" : ObjectId("5ec293782bc00b43b463b67c")
"status" : ["running"],
"name" : "name1 ",
"dcode" : "dc001",
"address" : "address1",
"city" : "city1"
col2:
"_id" : ObjectId("5ec296182bc00b43b463b68f"),
"scode" : ObjectId("5ec2933df6079743c0a2a1f8"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b463688b"),
"scode" : ObjectId("5ec2933df6079743c0a2a1ff"),
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
"_id" : ObjectId("5ec296182bc00b43b44fc6cb"),
"scode" :null,
"ycode" : ObjectId("5ec293782bc00b43b463b67c"),
"city" : "city1",
"lockedDate" : ISODate("2020-05-20T00:00:00Z"),
problemStatement:
I want to display name from col1 & count of documents from col2 according to ycode where scode is != null
Tried attempt:
db.col1.aggregate([
{'$match':{
city:'city1'
}
},
{
$lookup:
{
from: "col2",
let: {
ycode: "$_id",city:'$city'
},
pipeline: [
{
$match: {
scode:{'$ne':null},
lockedDate:ISODate("2020-05-20T00:00:00Z"),
$expr: {
$and: [
{
$eq: [
"$ycode",
"$$ycode"
]
},
{
$eq: [
"$city",
"$$city"
]
}
]
},
},
},
], as: "col2"
}
},
{'$unwind':'$col2'},
{'$count':'ycode'},
{
$project: {
name: 1,
status: 1,
}
},
])
now problem with this query is it either displays the count or project the name & status i.e if i run this query in the current format it gives {} if I remove {'$count':'ycode'} then it project the values but doesn't give the count and if I remove $project then i do get the count {ycode:2} but then project doesn't work but I want to achieve both in the result. Any suggestions
ORM: mongoose v>5, mongodb v 4.0
You can try below query :
db.col1.aggregate([
{ "$match": { city: "city1" } },
{
$lookup: {
from: "col2",
let: { id: "$_id", city: "$city" }, /** Create local variables from fields of `col1` but not from `col2` */
pipeline: [
{
$match: { scode: { "$ne": null }, lockedDate: ISODate("2020-05-20T00:00:00Z"),
$expr: { $and: [ { $eq: [ "$ycode", "$$id" ] }, { $eq: [ "$city", "$$city" ] } ] }
}
},
{ $project: { _id: 1 } } // Optional, But as we just need count but not the entire doc, holding just `_id` helps in reduce size of doc
],
as: "col2" // will be an array either empty (If no match found) or array of objects
}
},
{
$project: { _id: 0, name: 1, countOfCol2: { $size: "$col2" } }
}
])
Test : mongoplayground

Mongodb Group by get $max and count of max in value and percent of that group

I need to a group by on x field and get the max value of other fields. Yes, using $max we can get max repetitive value. But, I also need to get count $max value in percent/count too. In other words, how many times this $max value exist in that group. Kindly help.
example:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"l1_max" : { "$max" : "$l1" }
}
},
]
);
Here, I am getting l1_max = 'Computer' . But, I need it as 'Computer - (30%) Total 4/12'
Updated: 20/10/2019
#mickl : Thanks for the answer.
The field l1 is actually a referenced field. In normal find/project or mongoose populate(), it helps to get fields from other collection. Example:
if l1 is of type ObjectId then,
l1: {
_id, "4343434343sdsdsY",
name: "IT"
}
So l1.name will fetch name field from another collection in project/populate function.
I executed following code:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"count": { '$sum': 1 },
"l1_max" : { "$max" : "$l1" },
"l1_values": { $push: "$l1" }
}
},
{
$project: {
_id: 1,
total: 1,
l1 : {"_id": "$l1_max", "count": "$count", "percent": { $divide: [ { $size: { $filter: { input: "$l1_values", cond: { $eq: [ "$$this", "$l1_max" ] } } } },"$count"]}}
}
}
]
);
Answer is like below: But I also need referenced name field too.
{
"_id" : {
"name" : "xzy"
},
"total" : 35.0,
"l1" : {
"_id" : "4343920239201W",
"name" : "IT", // **MISSING**
"count" : 4.0,
"percent" : 0.25
}
}
Hope I was clear this time.
You need to capture all l1 values in your group and the calculate the percent using $divide, $filter and $size:
db.getCollection("test").aggregate(
[
{ "$match" : { "doc_id" : 1.0 } },
{ "$group" : {
"_id" : { "name" : "$name" },
"total" : { "$sum" : "$amount" },
"l1_max" : { "$max" : "$l1" },
"l1_values": { $push: "$l1" }
}
},
{
$project: {
_id: 1,
total: 1,
l1_max: 1,
l1_perc: {
$divide: [
{ $size: { $filter: { input: "$l1_values", cond: { $eq: [ "$$this", "$l1_max" ] } } } },
{ $size: "$l1_values" }
]
}
}
}
]
);
Mongo Playground

How to divide the elements inside array using mongodb aggregation

I have used aggregation to find below output.
Next step is to divide the values of elements present in each array.
{
"_id" : {
“type”:”xxx”,
"year" : 2019
},
"allvalues" : [
{
"label" : “used”,
"value" : 50
},
{
"label" : “total”,
"value" : 58
}
]
}
{
"_id" : {
“type”:”yyy”,
"year" : 2019
},
"allvalues" : [
{
"label" : “used”,
"value" : 63.214285714285715
},
{
"label" : “total”,
"value" : 59.535714285714285
}
]
}
How to write the query to divide used/total in each doc.
For first doc it is 50/58 and second doc it is 63.21/59.53.
The structure of json remains constant.
Output should look like below:
{
“type”:”xxx”,
"year" : 2019,
“result” : 0.8
}
{
“type”:”yyy”,
"year" : 2019,
“result” : 1.06
}
Add this in the aggregate pipeline after you get above input first you need to use $filter and $arrayElemAt to get used and total's value after that as divide doesn't give fixed decimal place, I've used $trunc to make value to 2 decimal fixed place
{
$project: {
_id: 1,
used: {
$arrayElemAt: [
{
$filter: {
input: "$allvalues",
as: "item",
cond: {
$eq: [
"$$item.label",
"used"
]
}
}
},
0
]
},
total: {
$arrayElemAt: [
{
$filter: {
input: "$allvalues",
as: "item",
cond: {
$eq: [
"$$item.label",
"total"
]
}
}
},
0
]
},
}
},
{
$project: {
_id: 1,
result: {
$divide: [
{
$trunc: {
$multiply: [
{
$divide: [
"$used.value",
"$total.value"
]
},
100
]
}
},
100
]
}
}
}