MongoDB count related documents (3 levels) - mongodb

I need to fast count related documents.
So, I have four collections
groups
{ "_id" : "g1", "name" : "group1" }
{ "_id" : "g2", "name" : "group2" }
courses
{ "_id" : "c1", "name" : "course1", "group_id" : "g1" }
{ "_id" : "c2", "name" : "course2", "group_id" : "g2" }
topics
{ "_id" : "t1", "name" : "top1c11", "course_id" : "c1" }
{ "_id" : "t2", "name" : "top1c12", "course_id" : "c1" }
{ "_id" : "t3", "name" : "top1c21", "course_id" : "c2" }
lessons
{ "_id" : "l1", "name" : "lesson111", "topic_id" : "t1" }
{ "_id" : "l2", "name" : "lesson112", "topic_id" : "t1" }
{ "_id" : "l3", "name" : "lesson121", "topic_id" : "t2" }
{ "_id" : "l4", "name" : "lesson211", "topic_id" : "t3" }
I need count all lessons of the specific group.
I tried to run the following aggregation, but I didn't wait for an response. (But it working for small amount of data)
db.getCollection('lessons').aggregate([
{
"$lookup": {
"from": "topics",
"let": { "topicId": "$topic_id" },
"pipeline": [
{
"$match": { "$expr": { "$eq": [ "$_id", "$$topicId" ] } }
},
{
"$lookup": {
"from": "courses",
"let": { "courseId": "$topic_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": [ "$course_id", "$$courseId" ] } } },
],
"as": "course"
},
},
{
"$unwind": "$course"
}
],
"as": "topic"
},
},
{
"$unwind" : "$topic"
},
{
"$match": {
"topic.course.group_id" : "g1"
}
},
{
$group: {
_id: "$course",
"amount": {$sum:1},
}
}
])
I believe this aggregation can be optimized. But I don`t sure that is a good approach to use aggregation framework for such purpose. If so, how can I optimize the aggregation.
Size of collections (test data):
courses: 30000
topics: 200000
lessons: 30000000
Now I use simple nested loops in my code to count lessons. This takes 10 seconds (for 3000 topics of the certain group).

Query1
not nested lookups (lookup and unwind)
match the group
lookup and unwind 3x, last lookup only counts the lessons, and uses pipeline lookup
group by group _id, to get the total lessons
Indexes that you need (all the foreignField)
courses.group_id
topics.course_id
lessons.topic_id
Test code here
groups.aggregate(
[{"$match":{"_id":"g1"}},
{"$lookup":
{"from":"courses",
"localField":"_id",
"foreignField":"group_id",
"as":"courses"}},
{"$unwind":"$courses"},
{"$lookup":
{"from":"topics",
"localField":"courses._id",
"foreignField":"course_id",
"as":"topics"}},
{"$unwind":"$topics"},
{"$lookup":
{"from":"lessons",
"localField":"topics._id",
"foreignField":"topic_id",
"pipeline":
[{"$group":{"_id":null, "lessons":{"$sum":1}}},
{"$set":{"id":"$_id", "_id":"$$REMOVE"}}],
"as":"lessons"}},
{"$set":
{"lessons":
{"$cond":
[{"$eq":["$lessons", []]}, 0,
{"$arrayElemAt":["$lessons.lessons", 0]}]}}},
{"$group":{"_id":"$_id", "totalLessons":{"$sum":"$lessons"}}}])
Query2
nested lookups (without unwind)
code is the same, just nested
Test code here
groups.aggregate(
[{"$match":{"_id":"g1"}},
{"$lookup":
{"from":"courses",
"localField":"_id",
"foreignField":"group_id",
"pipeline":
[{"$lookup":
{"from":"topics",
"localField":"_id",
"foreignField":"course_id",
"pipeline":
[{"$lookup":
{"from":"lessons",
"localField":"_id",
"foreignField":"topic_id",
"pipeline":
[{"$group":{"_id":null, "lessons":{"$sum":1}}},
{"$set":{"id":"$_id", "_id":"$$REMOVE"}}],
"as":"lessons"}},
{"$set":
{"lessons":
{"$cond":
[{"$eq":["$lessons", []]}, 0,
{"$arrayElemAt":["$lessons.lessons", 0]}]}}}],
"as":"topics"}},
{"$project":
{"_id":0, "totalLessons":{"$sum":"$topics.lessons"}}}],
"as":"courses"}},
{"$set":
{"courses":"$$REMOVE",
"totalLessons":
{"$cond":
[{"$eq":["$courses", []]}, 0,
{"$arrayElemAt":["$courses.totalLessons", 0]}]}}}])
If you can send some feedback on which one was faster.
If for 1 group its very fast, maybe remove the match, to do it for all groups, or allow from match to pass more many groups.

Solution from comment of Takis. Query1, adopted for 4.2
groups.aggregate(
[{"$match":{"_id":"g1"}},
{"$lookup":
{"from":"courses",
"localField":"_id",
"foreignField":"group_id",
"as":"courses"}},
{"$unwind":"$courses"},
{"$lookup":
{"from":"topics",
"localField":"courses._id",
"foreignField":"course_id",
"as":"topics"}},
{"$unwind":"$topics"},
{"$lookup":
{"from":"lessons",
"pipeline":
[{"$match":{"$expr":{"$eq":["$$ptopic", "$topic_id"]}}},
{"$group":{"_id":null, "lessons":{"$sum":1}}},
{"$set":{"id":"$_id", "_id":"$$REMOVE"}}],
"as":"lessons",
"let":{"ptopic":"$topics._id"}}},
{"$set":
{"lessons":
{"$cond":
[{"$eq":["$lessons", []]}, 0,
{"$arrayElemAt":["$lessons.lessons", 0]}]}}},
{"$group":{"_id":"$_id", "totalLessons":{"$sum":"$lessons"}}}])

Related

How can I group by a string instead of ObjectId in MongoDB aggregate?

I have two collections and a many-to-one relationship between them:
Product:
"_id" : ObjectId("61cc81c9585946c3b44f24411"),
"name" : "some random name",
"price" : 100,
"description" : "description",
"category_id" : ObjectId("61cc8100585946c3b44f2317d")
Category:
{
"_id" : ObjectId("61cc8100585946c3b44f2317d"),
"description" : "Category description",
"name" : "Electronics"
}
I would like to output the maximum product price for each category:
db.product.aggregate([
{ "$group": {
"_id": "$category_id",
"max": { "$max": "$price"}
}}
])
This works just fine as it prints me the following:
{ "_id" : ObjectId("61cc80fb585946c3b44f697c"), "max" : 62}
{ "_id" : ObjectId("61cc8100585946c3b44f697d"), "max" : 100}
But is there a way to get the "name" from the Category instead of its object id?
I know in SQL you would group by category_name but it does not seem to work here.
As suggested by #prasad, you should make use of $lookup stage after your $group stage.
db.product.aggregate([
{
"$group": {
"_id": "$category_id",
"max": {
"$max": "$price"
}
}
},
{
"$lookup": {
"from": "category",
"localField": "_id",
"foreignField": "_id",
"as": "categoryName",
}
},
{
"$set": {
"categoryName": {
"$arrayElemAt": [
"$categoryName.name",
0
]
}
}
}
])
Mongo Playground Sample

MongoDB - Apply similar lookup on multiple columns in main table

I'm a newbie to MongoDB, I'm trying to aggregate complete details of students in referencing with other collections.
students collection structure:
{
"_id" : ObjectId("5cc973dd008221192148177a"),
"name" : "James Paulson",
"teachers" : [
ObjectId("5cc973dd008221192148176f"),
ObjectId("5cc973dd0082211921481770")
],
"attenders": [
ObjectId("5cc973dd0082211921481732"),
ObjectId("5cc973dd008221192148173f")
]
}
staff collection structure:
{
"_id" : ObjectId("5cc973dd008221192148176f"),
"name" : "John Paul",
"subject" : [
"english",
"maths"
]
}
{
"_id" : ObjectId("5cc973dd0082211921481771"),
"name" : "Pattrick",
"subject" : [
"physics",
"history"
]
}
{
"_id" : ObjectId("5cc973dd0082211921481732"),
"name" : "Roger",
"subject" : [
"sweeper"
]
}
{
"_id" : ObjectId("5cc973dd008221192148173f"),
"name" : "Ken",
"subject" : [
"dentist"
]
}
This is the query I used for the retrieval of all teacher details of a particular student.
Query:
db.getCollection('students').aggregate([
{
$unwind: "$teachers"
},
{
$lookup:
{
from: 'staff',
localField: 'teachers',
foreignField: '_id',
as: 'teachers'
}
}
]);
Result:
{
"_id" : ObjectId("5cc973dd008221192148177a"),
"name" : "James Paulson",
"teachers" : [
{
"_id" : ObjectId("5cc973dd008221192148176f"),
"name" : "John Paul",
"subject" : [
"english",
"maths"
]
},
{
"_id" : ObjectId("5cc973dd008221192148176f"),
"name" : "Pattrick",
"subject" : [
"physics",
"history"
]
}
],
"attenders": [
ObjectId("5cc973dd0082211921481732"),
ObjectId("5cc973dd008221192148173f")
]
}
As you can see, the attenders array is also similar to teachers except the difference in column name in students table. So how to applying a similar query to the second column (attenders)? Also is there any way to select specific columns from the second table (like only _id and name from staff collection)?
Any help on this would be greatly appreciated.
You can use below aggregation with mongodb 3.6 and above
Firstly you don't need to use $unwind here as your field already contains array of ObjectIds. And to select the specific field from the referenced collection you can use the custom $lookup with pipeline and $project the fields inside it.
db.getCollection('students').aggregate([
{ "$lookup": {
"from": "staff",
"let": { "teachers": "$teachers" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$teachers" ] } } }
{ "$project": { "name": 1 }}
],
"as": "teachers"
}},
{ "$lookup": {
"from": "attenders",
"let": { "attenders": "$attenders" },
"pipeline": [
{ "$match": { "$expr": { "$in": [ "$_id", "$$attenders" ] } } }
],
"as": "attenders"
}}
])

exclude fields in $lookup aggregation

I am querying between 3 collections I want to exclude _id everywhere in output
My output is:
{
"_id" : ObjectId("5b6aed5f9bcdb5d4ae64aef5"),
"userID" : "1",
"skills" : [
{
"_id" : ObjectId("5b766b5f1365a4940bb6050f"),
"skillID" : "javaid",
"skillname" : "जावा",
"languageID" : "hindiid"
},
{
"_id" : ObjectId("5b766b8c1365a4940bb60535"),
"skillID" : "pythonid",
"skillname" : "पायथन",
"languageID" : "hindiid"
}
],
"gender" : {
"_id" : ObjectId("5b7687cd2a2329043e2383d5"),
"genderID" : "femaleid",
"gendername" : "महिला",
"languageID" : "hindiid"
}
}
Query:
db.User.aggregate([
{ "$match": { "userID":"1" }},
{ "$lookup":{
"from": "Skill",
"pipeline": [
{ "$match": { "languageID": "hindiid", "skillID": { "$in": [ "javaid","pythonid" ] }}},
],
"as": "skills"
}},
{ "$lookup": {
"from": "Gender",
"pipeline": [
{ "$match": { "languageID": "hindiid", "genderID" : "femaleid" }},
],
"as": "gender"
}},
{ "$unwind": { "path": "$gender", "preserveNullAndEmptyArrays": true }},
{ "$project": { "userID": 1, "skills": 1, "gender": 1 }}
])
In output for every object has _id.Example for skill list every object has _id i want exclude _id field every where. How I can exclude?
In mongodb 3.6 you can use projection ($project) inside $lookup pipeline... Something like this
db.User.aggregate([
{ "$match": { "userID":"1" }},
{ "$lookup":{
"from": "Skill",
"pipeline": [
{ "$match": { "languageID": "hindiid", "skillID": { "$in": [ "javaid","pythonid" ] }}},
{ "$project": { "_id": 0 }}
],
"as": "skills"
}}
])

MongoDB join data inside an array of objects

I have document like this in a collection called diagnoses :
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : ObjectId("58345e0e996d340bd8126149"),
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : ObjectId("5836bc0b291918eb42966320"),
"instructions" : "Take 1 daily, after meals."
}
]
}
The drug id inside the prescription object array is from a separate collection called drugs, see sample document below :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
}
I am trying to create a mongodb query using the native node.js driver to get a result like this:
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" :
{
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
...
]
}
Any advice on how to approach a similar result like this is much appreciated, thanks.
Using MongoDB 3.4.4 and newer
With the aggregation framework, the $lookup operators supports arrays
db.diagnoses.aggregate([
{ "$addFields": {
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
} },
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "drugs"
} },
{ "$addFields": {
"prescription": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "drug": {
"$arrayElemAt": [
"$drugs",
{
"$indexOfArray": [
"$drugs._id",
"$$this.drug"
]
}
]
} }
]
}
}
}
} },
{ "$project": { "drugs": 0 } }
])
For older MongoDB versions:
You can create a pipeline that first flattens the prescription array using the $unwind operator and a $lookup subsequent pipeline step to do a "left outer join" on the "drugs" collection. Apply another $unwind operation on the created array from the "joined" field. $group the previously flattened documents from the first pipeline where there $unwind operator outputs a document for each element in the prescription array.
Assembling the above pipeline, run the following aggregate operation:
db.diagnoses.aggregate([
{
"$project": {
"patientid": 1,
"doctorid": 1,
"medicalcondition": 1,
"diagnosis": 1,
"addmissiondate": 1,
"dischargedate": 1,
"bhtno": 1,
"prescription": { "$ifNull" : [ "$prescription", [ ] ] }
}
},
{
"$unwind": {
"path": "$prescription",
"preserveNullAndEmptyArrays": true
}
},
{
"$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription.drug"
}
},
{ "$unwind": "$prescription.drug" },
{
"$group": {
"_id": "$_id",
"patientid" : { "$first": "$patientid" },
"doctorid" : { "$first": "$doctorid" },
"medicalcondition" : { "$first": "$medicalcondition" },
"diagnosis" : { "$first": "$diagnosis" },
"addmissiondate" : { "$first": "$addmissiondate" },
"dischargedate" : { "$first": "$dischargedate" },
"bhtno" : { "$first": "$bhtno" },
"prescription" : { "$push": "$prescription" }
}
}
])
Sample Output
{
"_id" : ObjectId("582d43d18ec3f432f3260682"),
"patientid" : ObjectId("582aacff3894c3afd7ad4677"),
"doctorid" : ObjectId("582a80c93894c3afd7ad4675"),
"medicalcondition" : "high fever, cough, runny nose.",
"diagnosis" : "Viral Flu",
"addmissiondate" : "2016-01-12",
"dischargedate" : "2016-01-16",
"bhtno" : "125",
"prescription" : [
{
"drug" : {
"_id" : ObjectId("58345e0e996d340bd8126149"),
"genericname" : "Paracetamol Tab 500mg",
"type" : "X",
"isbrand" : false
},
"instructions" : "Take 2 daily, after meals."
},
{
"drug" : {
"_id" : ObjectId("5836bc0b291918eb42966320"),
"genericname" : "Paracetamol Tab 100mg",
"type" : "Y",
"isbrand" : false
},
"instructions" : "Take 1 daily, after meals."
}
]
}
In MongoDB 3.6 or later versions
It seems that
$lookup will overwrite the original array instead of merging it.
A working solution (a workaround, if you prefer) is to create a different field,
and then merge two fields, as shown below:
db.diagnoses.aggregate([
{ "$lookup": {
"from": "drugs",
"localField": "prescription.drug",
"foreignField": "_id",
"as": "prescription_drug_info"
} },
{ "$addFields": {
"merged_drug_info": {
"$map": {
"input": "$prescription",
"in": {
"$mergeObjects": [
"$$this",
{ "$arrayElemAt": [
"$prescription_drug_info._id",
"$$this._id"
] }
]
}
}
}
} }
])
This would add two more fields and the name of the desired field
will be merged_drug_info. We can then add $project stage to filter
out excessive fields and $set stage to rename the field:
...
{ "$set": { "prescription": "$merged_drug_info" } },
{ "$project": { "prescription_drug_info": 0, "merged_drug_info": 0 } }
...

Mongo aggregate nested array

I have a mongo collection with following structure
{
"userId" : ObjectId("XXX"),
"itemId" : ObjectId("YYY"),
"resourceId" : 1,
"_id" : ObjectId("528455229486ca3606004ec9"),
"parameter" : [
{
"name" : "name1",
"value" : 150,
"_id" : ObjectId("528455359486ca3606004eed")
},
{
"name" : "name2",
"value" : 0,
"_id" : ObjectId("528455359486ca3606004eec")
},
{
"name" : "name3",
"value" : 2,
"_id" : ObjectId("528455359486ca3606004eeb")
}
]
}
There can be multiple documents with the same 'useId' with different 'itemId' but the parameter will have same key/value pairs in all of them.
What I am trying to accomplish is return aggregated parameters "name1", "name2" and "name3" for each unique "userId" disregard the 'itemId'. so final results would look like for each user :
{
"userId" : ObjectId("use1ID"),
"name1" : (aggregatedValue),
"name2" : (aggregatedValue),
"name3" : (aggregatedVAlue)
},
{
"userId" : ObjectId("use2ID"),
"name1" : (aggregatedValue),
"name2" : (aggregatedValue),
"name3" : (aggregatedVAlue)
}
Is it possible to accomplish this using the aggregated methods of mongoDB ? Could you please help me to build the proper query to accomplish that ?
The simplest form of this is to keep things keyed by the "parameter" "name":
db.collection.aggregate(
// Unwind the array
{ "$unwind": "$parameter"},
// Group on the "_id" and "name" and $sum "value"
{ "$group": {
"_id": {
"userId": "$userId",
"name": "$parameter.name"
},
"value": { "$sum": "$parameter.value" }
}},
// Put things into an array for "nice" processing
{ "$group": {
"_id": "$_id.userId",
"values": { "$push": {
"name": "$_id.name",
"value": "$value"
}}
}}
)
If you really need to have the "values" of names as the field values, you can do the the following. But since you are "projecting" the fields/properties then you must specify them all in your code. You cannot be "dynamic" anymore and you are coding/generating each one:
db.collection.aggregate([
// Unwind the array
{ "$unwind": "$parameter"},
// Group on the "_id" and "name" and $sum "value"
{ "$group": {
"_id": {
"userId": "$userId",
"name": "$parameter.name"
},
"value": { "$sum": "$parameter.value"}
}},
// Project out discrete "field" names with $cond
{ "$project": {
"name1": { "$cond": [
{ "$eq": [ "$_id.name", "name1" ] },
"$value",
0
]},
"name2": { "$cond": [
{ "$eq": [ "$_id.name", "name2" ] },
"$value",
0
]},
"name3": { "$cond": [
{ "$eq": [ "$_id.name", "name3" ] },
"$value",
0
]},
}},
// The $cond put "0" values in there. So clean up with $group and $sum
{ "$group": {
_id: "$_id.userId",
"name1": { "$sum": "$name1" },
"name2": { "$sum": "$name2" },
"name3": { "$sum": "$name3" }
}}
])
So while the extra steps give you the result that you want ( well with a final project to change the _id to userId ), for my mind the short version is workable enough, unless you really do need it. Consider the output from there as well:
{
"_id" : ObjectId("53245016ea402b31d77b0372"),
"values" : [
{
"name" : "name3",
"value" : 2
},
{
"name" : "name2",
"value" : 0
},
{
"name" : "name1",
"value" : 150
}
]
}
So that would be what I would use, personally. But your choice.
Not sure if I got your question but if the name field can contain only "name1", "name2", "name3" or at least you are only interested in this values, one of the possible queries could be this one:
db.aggTest.aggregate(
{$unwind:"$parameter"},
{$project: {"userId":1, "parameter.name":1,
"name1" : {"$cond": [{$eq : ["$parameter.name", "name1"]}, "$parameter.value", 0]},
"name2" : {"$cond": [{$eq : ["$parameter.name", "name2"]}, "$parameter.value", 0]},
"name3" : {"$cond": [{$eq : ["$parameter.name", "name3"]}, "$parameter.value", 0]}}},
{$group : {_id : {userId:"$userId"},
name1 : {$sum:"$name1"},
name2 : {$sum:"$name2"},
name3 : {$sum:"$name3"}}})
It firsts unwinds the parameter array, then separates name1, name2 and name3 values into different columns. There's a simple conditional statement for that. After that we can easily aggreagate by the new columns.
Hope it helps!