I want to select some specific fields from a particular object in a nested array, achieved through mongoose/mongo.
Playground Link
Consider the data:
[
{
"_id": ObjectId("5ff4b728b6af610f0851d2a6"),
"totalScore": 500,
"totalCompleted": 100,
"monthly": [
{
year: 2021,
month: 8,
attempted: 10,
completed: 5,
score: 20,
}
],
},
]
I want to first get all the documents, and then inside the "monthly", I want to select only the ones which match month = 8, and return only the "score" field and ignore rest of the fields like "attempted", "completed", etc.
I have tried the following query so far:
db.collection.find({},
{
totalScore: 1,
"monthly": {
$elemMatch: {
year: 2021,
month: 8,
},
},
})
It returns all the keys of the entire "monthly" object. Like so:
[
{
"_id": ObjectId("5ff4b728b6af610f0851d2a6"),
"monthly": [
{
"attempted": 10,
"completed": 5,
"month": 8,
"score": 20,
"year": 2021
}
],
"totalScore": 500
},
]
But, what I want, is to only select the "score" field from the "monthly".
So the result data would be:
[
{
"_id": ObjectId("5ff4b728b6af610f0851d2a6"),
"monthly": [
{
"score": 20,
}
],
"totalScore": 500
},
How should I approach this problem?
This can be done with a simple aggregation using $map and $filter:
db.collection.aggregate([
{
$project: {
totalScore: 1,
monthly: {
$map: {
input: {
$filter: {
input: "$monthly",
as: "item",
cond: {
$eq: [
"$$item.month",
8
]
}
}
},
as: "item",
in: {
score: "$$item.score"
}
}
}
}
}
])
Example on mongoplayground: https://mongoplayground.net/p/5PbR49Ufxb5
Related
I am trying to find the missing documents in MongoDB. Scenario is like this: I have a collection where the documents have a hour-stamp field. I want to find which hours are missing given the time range.
Since I am writing this question on Metabase, I am limited to use only one aggregation pipeline, meaning I can't use $out to make temperate collection and do $lookup for join.
I can only fill in the code of db.collection.aggregate(my code)
Any idea how can I achieve this? Thanks a lot!
Was able to achieve this so just sharing my solution.
Idea:
Generate an array for the hours needed to be checked. Notice I use
hour-diff from current hour, so I can dynamically check if (-9
hours) is missing. Reason for doing this is that I cannot find a way
to programmatically generate this array using absolute hour-stamp
(2022-07-11 10:00:00).
Calculate the hour-diff from current of the data's hour-stamp.
Use $setDifference to find the missing hours.
Calculate the absolute hour-stamp from the hour-diff value to get the missing hours.
Works for my need, and hope this will help someone.
Code snippet (I use this for finding missing hours between -6 to -30 hours for each data_source) :
db.getCollection(<collection_name>).aggregate(
[
{ $project: {
_id: 1,
data_source: 1,
available_date_time: { $toDate: "$available_date_time"},
current_hour: { $dateFromString: { dateString: { $dateToString: { format: "%Y-%m-%dT%H", date: ISODate() } }, format: "%Y-%m-%dT%H" } },
}},
{ $project: {
_id:1,
data_source:1,
available_date_time: 1,
current_hour: 1,
current_hour_minus_6hr: { $subtract: [ "$current_hour", { $multiply: [6, 60, 60, 1000] }] },
current_hour_minus_30hr: { $subtract: [ "$current_hour", { $multiply: [30, 60, 60, 1000] }] },
}},
{ $project: {
_id:1,
data_source:1,
available_date_time: 1,
current_hour: 1,
past_6hr_comp: { $subtract: [ { $toDate: "$available_date_time"}, "$current_hour_minus_6hr" ] },
past_30hr_comp: { $subtract: [ { $toDate: "$available_date_time"}, "$current_hour_minus_30hr" ] },
}},
{ $match: {
$and: [
{ past_30hr_comp: { $gte: 0 } },
{ past_6hr_comp: { $lte: 0} }
]
}},
{ $project: {
_id: 1,
data_source:1,
available_date_time: 1,
current_hour: 1,
hour_diff_from_current: { $divide: [ {$subtract: [ "$current_hour", "$available_date_time" ] }, 3600000 ] }
}},
{ $group: {
_id: { data_source: "$data_source" },
count: { $sum: 1 },
available_hour_diff_set: { $addToSet: "$hour_diff_from_current" },
current_hour: { $first: "$current_hour" }
}},
{ $project: {
_id: 0,
data_source: "$_id.data_source",
available_hour_count: "$count",
available_hour_diff_set: "$available_hour_diff_set",
required_hour_diff_set: [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6],
current_hour: "$current_hour"
}},
{ $project: {
data_source: 1,
available_hour_count: 1,
unavailable_hour_diff_set: { $setDifference: [ "$required_hour_diff_set", "$available_hour_diff_set" ] },
current_hour:1
}},
{ $unwind: "$unavailable_hour_diff_set" },
{ $project: {
data_source: 1,
available_hour_count: 1,
current_hour: 1,
unavailable_hour_diff: "$unavailable_hour_diff_set",
}},
{ $project: {
data_source: 1,
available_hour_count: 1,
current_hour: 1,
unavailable_hour_diff: "$unavailable_hour_diff",
missing_hour: { $subtract: [ "$current_hour", { $multiply: [ "$unavailable_hour_diff", 60, 60, 1000 ] } ] }
}}
]
I have a collection from which I need specific obj e.g. notes.blok2 and notes.curse5 as an object, not as an array
{
"year":2020,
"grade":4,
"seccion":"A",
"id": 100,
"name": "pedro",
"notes":[{"curse":5,
"block":1,
"score":{ "a1": 5,"a2": 10, "a3": 15}
},{"curse":5,
"block":2,
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
]
}
My query
notas.find({
"$and":[{"grade":1},{"seccion":"A"},{"year":2020}]},
{"projection":{ "grade":1, "seccion":1,"name":1,"id":1,
"notes":{"$elemMatch":{"block":2,"curse":5}},"notes.score":1} })
It works but returns notes like array
{
"_id": "55",
"id": 100,
"grade": 5,
"name": "pedro",
"seccion": "A",
"notes": [
{"score": { "b1": 10,"b2": 20, "b3": 30} }
]
}
But I NEED LIKE THIS: score at the same level as others and if doesn't exist show empty "score":{}
{
"year":2020,
"grade":5,
"seccion":"A",
"id": 100,
"name": "pedro",
"score":{ "b1": 10,"b2": 20, "b3": 30}
}
Demo - https://mongoplayground.net/p/XlJqR2DYW1X
You can use aggregation query
db.collection.aggregate([
{
$match: { // filter
"grade": 1,
"seccion": "A",
"year": 2020,
"notes": {
"$elemMatch": {
"block": 2,
"curse": 5
}
}
}
},
{ $unwind: "$notes" }, //break into individual documents
{
$match: { // match query on individual note
"notes.block": 2,
"notes.curse": 5
}
},
{
$project: { // projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": "$notes.score"
}
}
])
Update
Demo - https://mongoplayground.net/p/mq5Kue3UG42
Use $filter
db.collection.aggregate([
{
$match: {
"grade": 1,
"seccion": "A",
"year": 2020
}
},
{
$set: {
"score": {
"$filter": {
"input": "$notes",
"as": "note",
"cond": {
$and: [
{
$eq: [ "$$note.block",3]
},
{
$eq: [ "$$note.curse", 5 ]
}
]
}
}
}
}
},
{
$project: {
// projection
"grade": 1,
"seccion": 1,
"name": 1,
"id": 1,
"score": {
"$first": "$score.score"
}
}
}
])
If you want empty object for score when match not found you can do -
Demo - https://mongoplayground.net/p/dumax58kgrc
{
$set: {
score: {
$cond: [
{ $size: "$score" }, // check array length
{ $first: "$score" }, // true - take 1st
{ score: {} } // false - set empty object
]
}
}
},
In my db I have a nested array of elements inside each document containing items, in the following form:
elements:[
{
"elem_id": 12,
items: [ {"i_id": 1, "type": x}, {"i_id": 2, "type": y}, {"i_id": 3, "type": x}]
},
{
"elem_id": 13,
items: [ {"i_id": 4, "type": x}, {"i_id": 5, "type": x}]
}
]
I am trying to return all elements that have items of different types, meaning I would get back only:
{
"elem_id": 12,
items: [ {"i_id": 1, "type": x}, {"i_id": 2, "type": y}, {"i_id": 3, "type": x}]
}
since there are items of type x and of type y.
I think I need to iterate the items array and compare the type of every item in the array to the types of the previous items but I can't figure out how to do this in aggregation.
Just to note - I am using Redash and so I can't include any JS in the query.
Thank you for the assistance!
Try this:
db.elements.aggregate([
{ $unwind: "$elements" },
{
$addFields: {
"count": { $size: "$elements.items" },
"uniqueValues": {
$reduce: {
input: "$elements.items",
initialValue: [{ $arrayElemAt: ["$elements.items.type", 0] }],
in: {
$setUnion: ["$$value", ["$$this.type"]]
}
}
}
}
},
{
$match: {
$expr: {
$eq: ["$count", { $size: "$uniqueValues" }]
}
}
}
]);
Output:
{
"_id" : ObjectId("603f8f05bcece4372062bcea"),
"elements" : {
"elem_id" : 12,
"items" : [
{
"i_id" : 1,
"type" : 1
},
{
"i_id" : 2,
"type" : 2
},
{
"i_id" : 3,
"type" : 3
}
]
},
"count" : 3,
"uniqueValues" : [1, 2, 3]
}
You can simplify the answer a bit (no need to use $reduce or $addFields):
db.collection.aggregate([
{$unwind: "$elements"},
{$match: {
$expr: {$gt:[
{$size: {$setIntersection: ["$elements.items.type", "$elements.items.type"]}},
1
]}
}}
])
See how it works on the playground example
I have MongoDB collection items with following document:
{
"values": [
{ "number1": 5, "number2": 6, "anotherProp": "...", "anotherProp2": "..." },
{ "number1": 8, "number2": 1, "anotherProp": "...", "anotherProp2": "..." }
]
}
Is there any way to add sum property to each item of values (sum = number1 + number2)? I would like to avoid naming all other properties (number1, number2, anotherProp, anotherProp2, ...), only add new one (sum). My current solution is:
db.items.aggregate([{
$project: {
values: {
$map: {
input: "$values",
as: "v",
in: {
sum: {$add: ["$$v.number1", "$$v.number2"]},
number1: "$$v.number1", // This and next 3 lines I would like to omit.
number2: "$$v.number2",
anotherProp: "$$v.anotherProp",
anotherProp2: "$$v.anotherProp2"
}
}
}
}
}])
Desired result is:
{
"values": [
{ "number1": 5, "number2": 6, "anotherProp": "...", "anotherProp2": "...", "sum": 11 },
{ "number1": 8, "number2": 1, "anotherProp": "...", "anotherProp2": "...", "sum": 9 }
]
}
Is there any way to do this? I tried use $addFields instead of $project, however result is same.
Yes, you can use $mergeObjects
db.collection.aggregate([
{
$project: {
values: {
$map: {
input: "$values",
as: "v",
in: {
"$mergeObjects": [
{
sum: {
$add: [
"$$v.number1",
"$$v.number2"
]
}
},
"$$v"
]
}
}
}
}
}
])
MongoPlayground
I have a MongoDB collection that contains a set of documents. Each document has an ISODate date and an integer id (not _id). id: X is said to exist for date: D if there is a document in the collection with field values { id: X, date: D }. So, for example:
{ id: 1, date: 1/1/2000 }
{ id: 1, date: 1/2/2000 }
{ id: 1, date: 1/3/2000 }
{ id: 1, date: 1/4/2000 }
{ id: 2, date: 1/2/2000 }
{ id: 2, date: 1/3/2000 }
{ id: 3, date: 1/3/2000 }
I would like to track ids over time as they are created and destroyed day-to-day. Using the above data, over the date range 1/1/2000 to 1/4/2000:
1/1/2000: id 1 is created
1/2/2000: id 2 is created
1/3/2000: id 3 is created
1/4/2000: id 2 is destroyed
1/4/2000: id 3 is destroyed
I think the best way to solve this would be to loop day by day, see what ids exist between today and the next day, and perform a set difference. For example, to get the set of ids created and destroyed on 1/2/2000, I need to perform two set differences between arrays for either day:
var A = [ <ids that exist on 1/1/2000> ];
var B = [ <ids that exist on 1/2/2000> ];
var created_set = set_difference(B, A); // Those in B and not in A
var destroyed_set = set_difference(A, B); // Those in A and not in B
I can use a find() command to get cursors for A and B, but I do not know how to perform the set_difference between two cursors.
My other option was to use an aggregation pipeline, but I cannot think about how to formulate the pipeline in such a way that I can use the $setDifference operator.
As a MongoDB novice, I am sure I'm thinking about the problem the wrong way. Surely this is something that can be done? What am I missing?
db.mystuff.aggregate([
{$group: {_id: '$id', created: {$first: '$date'}, destroyed: {$last: '$date'}}}
])
Suppose you have the following sample collection:
db.collection.insert([
{ id: 1, date: ISODate("2000-01-01") },
{ id: 1, date: ISODate("2000-01-02") },
{ id: 1, date: ISODate("2000-01-03") },
{ id: 1, date: ISODate("2000-01-04") },
{ id: 2, date: ISODate("2000-01-02") },
{ id: 2, date: ISODate("2000-01-03") },
{ id: 3, date: ISODate("2000-01-03") }
]);
The following aggregation will give you some direction towards what you are trying to achieve using the $setDifference operator:
var start = new Date(2000, 0, 1);
var end = new Date(2000, 0, 2)
db.collection.aggregate([
{
"$match":{
"date": {
"$gte": start,
"$lte": end
}
}
},
{
$group: {
_id: "$date",
"A": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", start ] },
"$id",
false
]
}
},
"B": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", end ] },
"$id",
false
]
}
}
}
},
{
"$project": {
"A": {
"$setDifference": [ "$A", [false] ]
},
"B": {
"$setDifference": [ "$B", [false] ]
}
}
},
{
"$project": {
"_id": 0,
"date": "$_id",
"created_set": {
"$setDifference": [ "$B", "$A" ]
},
"destroyed_set": {
"$setDifference": [ "$A", "$B" ]
}
}
}
]);
Output:
{
"result" : [
{
"date" : ISODate("2000-01-02T00:00:00.000Z"),
"created_set" : [2, 1],
"destroyed_set" : []
},
{
"date" : ISODate("2000-01-01T00:00:00.000Z"),
"created_set" : [],
"destroyed_set" : [1]
}
],
"ok" : 1
}