How to compare two object elements in a mongodb array - mongodb

{
"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")
}
]
}

Related

How to fetch records based on the max date in mongo document array

Below is the document which has an array name datum and I want to filter the records based on group by year and filter by the types and max date.
{
"_id" : ObjectId("5fce46ca6ac9808276dfeb8c"),
"year" : 2018,
"datum" : [
{
"Type" : "1",
"Amount" : NumberDecimal("100"),
"Date" : ISODate("2018-05-30T00:46:12.784Z")
},
{
"Type" : "1",
"Amount" : NumberDecimal("300"),
"Date" : ISODate("2023-05-30T00:46:12.784Z")
},
{
"Type" : "2",
"Amount" : NumberDecimal("340"),
"Date" : ISODate("2025-05-30T00:46:12.784Z")
},
{
"Type" : "3",
"Amount" : NumberDecimal("300"),
"Date" : ISODate("2021-05-30T00:46:12.784Z")
}
]
}
The aggregate Query I tried.
[{$group: {
_id :"$year",
RecentValue :
{
$sum: {
$reduce: {
input: '$datum',
initialValue: {},
'in': {
$cond:
[
{
$and:
[
{$or:[
{ $eq: [ "$$this.Type", '2' ] },
{$eq: [ "$$this.Type", '3' ] }
]},
{ $gt: [ "$$this.Date", "$$value.Date" ] },
]
}
,
"$$this.Amount",
0
]
}
}
}
}
}}]
the expected output would be which having the max date "2025-05-30T00:46:12.784Z"
{
_id :2018,
RecentValue : 340
}
Please let me know what mistake I did in the aggregate query.
You can get max date before $group stage,
$addFields to get document that having max date from, replaced $or with $in condition and corrected return value
$group by year and sum Amount
db.collection.aggregate([
{
$addFields: {
datum: {
$reduce: {
input: "$datum",
initialValue: {},
"in": {
$cond: [
{
$and: [
{ $in: ["$$this.Type", ["2", "3"]] },
{ $gt: ["$$this.Date", "$$value.Date"] }
]
},
"$$this",
"$$value"
]
}
}
}
}
},
{
$group: {
_id: "$year",
RecentValue: { $sum: "$datum.Amount" }
}
}
])
Playground

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

Lookup and aggregate multiple levels of subdocument in Mongodb

I've tried many answers to similar problems using $lookup, $unwind, and $match, but I can't get this to work for my sub-sub-subdocument situation.
I have this collection, Things:
{
"_id" : ObjectId("5a7241f7912cfc256468cb27"),
"name" : "Fortress of Solitude",
"alias" : "fortress_of_solitude",
},
{
"_id" : ObjectId("5a7247ec548c9ad042f579e2"),
"name" : "Batcave",
"alias" : "batcave",
},
{
"_id" : ObjectId("6a7247bc548c9ad042f579e8"),
"name" : "Oz",
"alias" : "oz",
},
and this one-document collection, Venues:
{
"_id" : ObjectId("5b9acabbbf71f39223f8de6e"),
"name" : "The Office",
"floors" : [
{
"name" : "1st Floor",
"places" : [
{
"name" : "Front Entrance",
"alias" : "front_entrance"
}
]
},
{
"name" : "2nd Floor",
"places" : [
{
"name" : "Batcave",
"alias" : "batcave"
},
{
"name" : "Oz",
"alias" : "oz"
}
]
}
]
}
I want to return all the Things, but with the Venue's floors.places.name aggregated with each Thing if it exists if the aliases match between Things and Venues. So, I want to return:
{
"_id" : ObjectId("5a7241f7912cfc256468cb27"),
"name" : "Fortress of Solitude",
"alias" : "fortress_of_solitude",
<-- nothing added here because
<-- it's not found in Venues
},
{
"_id" : ObjectId("5a7247ec548c9ad042f579e2"),
"name" : "Batcave",
"alias" : "batcave",
"floors" : [ <-- this should be
{ <-- returned
"places" : [ <-- because
{ <-- the alias
name" : "Batcave" <-- matches
} <-- in Venues
] <--
} <--
] <--
},
{
"_id" : ObjectId("6a7247bc548c9ad042f579e8"),
"name" : "Oz",
"alias" : "oz",
"floors" : [ <-- this should be
{ <-- returned
"places" : [ <-- because
{ <-- the alias
name" : "Oz" <-- matches
} <-- in Venues
] <--
} <--
] <--
}
I've gotten as far as the following query, but it only returns the entire Venues.floors array as an aggregate onto each Thing, which is way too much extraneous data aggregated. I just want to merge each relevant floor.place sub-subsubdocument from Venues into its corresponding Thing if it exists in Venues.
db.getCollection('things').aggregate([
{$lookup: {from: "venues",localField: "alias",foreignField: "floors.places.alias",as: "matches"}},
{
$replaceRoot: { newRoot: { $mergeObjects: [ { $arrayElemAt: [ "$matches", 0 ] }, "$$ROOT" ] } }
},
{ $project: { matches: 0 } }
])
I'm struggling with existing answers, which seem to change at MongoDB version 3.2, 3.4, 3.6, or 4.2 to include or not include $unwind, $pipeline, and other terms. Can someone explain how to get a sub-sub-subdocument aggregated like this? Thanks!
You can try this :
db.things.aggregate([
{
$lookup:
{
from: "venues",
let: { alias: "$alias" },
pipeline: [
{ $unwind: { path: "$floors", preserveNullAndEmptyArrays: true } },
{ $match: { $expr: { $in: ['$$alias', '$floors.places.alias'] } } },
/** Below stages are only if you've docs like doc 2 in Venues */
{ $addFields: { 'floors.places': { $filter: { input: '$floors.places', cond: { $eq: ['$$this.alias', '$$alias'] } } } } },
{ $group: { _id: '$_id', name: { $first: '$name' }, floors: { $push: '$floors' } } },
{$project : {'floors.places.alias': 1, _id :0}} // Optional
],
as: "matches"
}
}
])
Test : MongoDB-Playground
Since MongoDB v3.6, we may perform uncorrelated sub-queries which gives us more flexibility to join two collections.
Try this:
db.things.aggregate([
{
$lookup: {
from: "venues",
let: {
"alias": "$alias"
},
pipeline: [
{
$unwind: "$floors"
},
{
$project: {
_id: 0,
places: {
$filter: {
input: "$floors.places",
cond: {
$eq: [
"$$alias",
"$$this.alias"
]
}
}
}
}
},
{
$match: {
"places.0": {
$exists: true
}
}
},
{
$unset: "places.name"
}
],
as: "floors"
}
}
])
MongoPlayground

$divide elements of Embedded Documents - MongoDB Aggregation

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

How can I get this result in mongodb?

I have this data structure:
"_id" : "121212",
"terms" : [
{
"term" : "hi",
"tf" : 2
},
{
"term" : "you",
"tf" : 1
}
]
}
and making this query:
db.foo.aggregate( [
{
$match : { _id : "121212" }
},
{
$project:{ terms:1 }
},
{
$unwind: "$terms"
}
]).pretty();
I have come to get this result in my db:
{
"_id" : "121212",
"terms" : {
"term" : "hi",
"tf" : 2
}
}
{
"_id" : "121212",
"terms" : {
"term" : "you",
"tf" : 1
}
}
but is there any way to get a result like this?:
{
"_id" : "121212",
"term" : "hi",
"tf" : 2
}
{
"_id" : "121212",
"term" : "you",
"tf" : 1
}
I have tried to build the query with $ replaceRoot: {newRoot: "$ terms"}, but after I can't select the _id field anymore.
Well, you can use the $map and $mergeObjects to do this beautifully.
[
{ "$match":{"_id":"121212"}},
{
"$addFields":{
"terms":{
"$map":{
"input":"$terms",
"in":{
"$mergeObjects":[
"$$this",
{
"_id":"$_id"
}
]
}
}
}
}
}
]
If you really need to deconstruct the "terms" array, then add the $unwind: "$terms" to the pipeline.
You can achieve by using $project stage at the end of the pipeline
db.foo.aggregate([
{ "$match" : { "_id": "121212" } },
{ "$unwind": "$terms" },
{ "$project": { "term": "$terms.term", "tf": "$terms.tf" }}
])
Output
[
{
"_id": "121212",
"term": "hi",
"tf": 2
},
{
"_id": "121212",
"term": "you",
"tf": 1
}
]
Check it here
You need to use $mergeObjects inside $replaceRoot:
db.foo.aggregate( [
{
$match : { _id : "121212" }
},
{
$project:{ terms:1 }
},
{
$unwind: "$terms"
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: [ { _id: "$_id" }, "$terms" ]
}
}
}
]).pretty();
Just to complete the range of options:
db.foo.aggregate([
{ "$match" : { "_id": "121212" } }, // filter by "_id"
{ "$addFields": { "terms._id": "$_id" } }, // copy "_id" field into terms
{ "$unwind": "$terms" }, // flatten the "terms" array
{ "$replaceRoot": { "newRoot": "$terms" } } // move the contents of the "terms" field up to the root level
])