How can i optimize my query i have written to find the Users and there last order details using aggregate, it shows me timeout as the dataset is large - mongodb

I have a query as below, what it does it creates a link between two documents and find the last order date and users details like email, phone, etc. but on large data set it shows me timeout error any help would be much appreciated, and thanks in advance for the help
db.users.aggregate([
{
"$lookup": {
"from": "orders",
"let": {
"id": "$_id"
},
"pipeline": [
{
"$addFields": {
"owner": {
"$toObjectId": "$owner"
}
}
},
{
"$match": {
$expr: {
$eq: [
"$owner",
"$$id"
]
}
}
},
],
"as": "orders"
}
},
{
"$unwind": {
path: "$orders",
preserveNullAndEmptyArrays: false,
includeArrayIndex: "arrayIndex"
}
},
{
"$group": {
"_id": "$_id",
"order": {
"$last": "$orders.createdAt"
},
"userInfo": {
"$mergeObjects": {
name: "$name",
email: "$email",
phone: "$phone",
orderCount: "$orderCount"
}
}
}
},
{
"$project": {
name: "$userInfo.name",
email: "$userInfo.email",
phone: "$userInfo.phone",
orderCount: "$userInfo.orderCount",
lastOrder: "$order",
}
}
]
)
my documents look like the following for orders
{
"_id": ObjectId("607fbeeb0a752a66a7af40eb"),
"address": {
"loc": [
-1,
3
],
"_id": "5d35d55d3d081f486d0d401c",
"apartment": "",
"description": "ACcdg dfef"
},
"approvedAt": ISODate("2021-04-21T11:28:05.295+05:30"),
"assignedAt": null,
"billingAddress": {
"description": ""
},
"createdAt": ISODate("2021-04-21T11:28:04.449+05:30"),
"creditCard": "",
"deliveryDate": "04/21/21",
"deliveryDateObj": ISODate("2021-04-21T12:27:58.746+05:30"),
"owner": "609bd5831b912947ea51a9ac",
"products": [
"5a070c079b"
],
"updatedAt": ISODate("2021-04-21T11:28:05.295+05:30"),
}
and for users, it is like below
{
"_id": ObjectId("609bd5831b912947ea51a9ac"),
"updatedAt": ISODate("2021-05-12T18:47:55.291+05:30"),
"createdAt": ISODate("2021-05-12T18:47:55.213+05:30"),
"email": "1012#gmail.com",
"phone": "123",
"dob": "1996-04-10",
"password": "",
"stripeID": "",
"__t": "Customer",
"name": {
"first": "A",
"last": "b"
},
"orderCount": 1,
"__v": 0,
"forgottenPassword": ""
}

convert _id to string in lookup's let and you can remove $addFields from lookup pipeline
add $project stage in lookup pipeline and show only required fields
$project to show required fields and get last / max createdAt date use $max, you don't need to $unwind and $group operation
db.users.aggregate([
{
$lookup: {
from: "orders",
let: { id: { $toString: "$_id" } },
pipeline: [
{ $match: { $expr: { $eq: ["$owner", "$$id"] } } },
{
$project: {
_id: 0,
createdAt: 1
}
}
],
"as": "orders"
}
},
{
$project: {
email: 1,
name: 1,
orderCount: { $size: "$orders" },
phone: 1,
lastOrder: { $max: "$orders.createdAt" }
}
}
])
Playground
SUGGESTION:
You can save owner id in orders as objectId instead of string and whenever new order arrive store it as objectId, you can prevent conversation operator $toString operation
create an index in owner field to make lookup process faster.

I have figured out that after using createIndex for the owner field which is used to compare the owner in the orders from the users _id filed, so just after adding an db.orders.createIndex({ owner: 1 }), the query will run much faster and smoother

Related

Find one user then get their ranking based on their total points using MongoDB

So I got the following data:
Users collection
{
_id: ObjectId("62a2a0422ec90fea68390aaa"),
name: 'Robert Yamashita',
username: 'robyama',
email: 'robert.yamashita#rocketmail.com',
},
{
_id: ObjectId("62a2a0452ec90fea68390aad"),
name: 'Charles X',
username: 'cvx',
email: 'charles.xxx#rocketmail.com',
}
Points collection
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0422ec90fea68390aaa"),
action: 'Liked a post',
points: 10,
}
{
userId: ObjectId("62a2a0452ec90fea68390aad"),
action: 'Liked a comment',
points: 5,
}
I created a pipeline to get the total points of username robyama using the following query:
db.users.aggregate([
{ $match: { username: 'robyama' } },
{
$lookup: {
from: 'points',
localField: '_id',
foreignField: 'user',
as: 'userPoints'
}
},
{
$unwind: '$userPoints'
},
{
$group: {
_id: {
name: '$name',
email: '$email',
username: '$username',
},
count: { $sum: '$userPoints.points' }
}
}
]);
I got the following result:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
}
This is exactly what I needed but I wanted to add a ranking field to the returned query since Robert has 20 points and Charles only has 5. So ideally I want the result to be this:
{
"_id": {
"name": "Robert Yamashita",
"email": "robert.yamashita#rocketmail.com",
"username": "robyama",
},
"count": 20
"rank": 1
}
What should I add to my pipeline to get the above output? Any help would be greatly appreciated!
Here's another way to do it. There's only one "$lookup" with one embedded "$group" so it should be fairly efficient. The "$project" seems a bit contrived, but it gives the output in the format you want.
db.users.aggregate([
{
"$match": {
"username": "robyama"
}
},
{
"$lookup": {
"from": "points",
"as": "sortedPoints",
"pipeline": [
{
"$group": {
"_id": "$userId",
"count": {"$sum": "$points"}
}
},
{"$sort": {"count": -1}}
]
}
},
{
"$unwind": {
"path": "$sortedPoints",
"includeArrayIndex": "idx"
}
},
{
"$match": {
"$expr": {
"$eq": ["$_id", "$sortedPoints._id"]
}
}
},
{
"$project": {
"_id": {
"name": "$name",
"username": "$username",
"email": "$email"
},
"count": "$sortedPoints.count",
"rank": {
"$add": ["$idx", 1]
}
}
}
])
Try it on mongoplayground.net.
Well, this is one way of doing it.
Perform join using $lookup and calculate counts for each user.
Sort the elements by counts in desc order.
Group documents by _id as NULL and push them all in an array.
Unwind the array, along with getting row numbers.
Find your required document and calculate the rank using row number.
db.users.aggregate([
{
$lookup: {
from: "points",
localField: "_id",
foreignField: "userId",
as: "userPoints"
}
},
{
$unwind: "$userPoints"
},
{
$group: {
_id: {
name: "$name",
email: "$email",
username: "$username",
},
count: {
$sum: "$userPoints.points"
}
}
},
{
"$sort": {
count: -1
}
},
{
"$group": {
"_id": null,
"docs": {
"$push": "$$ROOT",
}
}
},
{
"$unwind": {
path: "$docs",
includeArrayIndex: "rownum"
}
},
{
"$match": {
"docs._id.username": "robyama"
}
},
{
"$addFields": {
"docs.rank": {
"$add": [
"$rownum",
1
]
}
}
},
{
"$replaceRoot": {
"newRoot": "$docs"
}
}
])
This is the playground link.

Mongoose- Group By And Populate : Get unique list of users a person had been talking to like whatsapp first screen where you have all previous users

i have been working on a chat app and i need to display the list of the users having conversation with the a user. more like the first screen of the whatsapp where you have the list of all users who texted you.
my messagesSchema is as follows:
from : {type : mongoose.Types.ObjectId, ref : "Users"},
to : {type : mongoose.Types.ObjectId, ref : "Users"},
messageType : String,
messageContent :String,
images : [Object],
poster : Object,
status : String,
date: { type: Date, default: Date.now }
i am using nodejs and i have tried so many agrregations, group by and other mongoose queries but fail
please help me with the query to get all the different unique users a person have been having a conversation with their single last message and populate the 'to' and 'from'.
Data:
[
{
"_id": ObjectId("100000000000000000000000"),
"from": ObjectId("100000000000000000000000"),
"to": ObjectId("200000000000000000000000"),
"date": ISODate("2021-09-03T11:23:25.184Z"),
"messageContent": "111"
},
{
"_id": ObjectId("200000000000000000000000"),
"from": ObjectId("200000000000000000000000"),
"to": ObjectId("100000000000000000000000"),
"date": ISODate("2021-09-02T11:23:25.184Z"),
"messageContent": "222"
},
{
"_id": ObjectId("300000000000000000000000"),
"from": ObjectId("300000000000000000000000"),
"to": ObjectId("100000000000000000000000"),
"date": ISODate("2021-09-04T11:23:25.184Z"),
"messageContent": "333"
},
{
"_id": ObjectId("400000000000000000000000"),
"from": ObjectId("300000000000000000000000"),
"to": ObjectId("400000000000000000000000"),
"date": ISODate("2021-09-05T11:23:25.184Z"),
"messageContent": "444"
}
]
Aggregate: (you are ObjectId("100000000000000000000000"))
db.collection.aggregate([
{
$match: {
"$or": [
{ "from": ObjectId("100000000000000000000000") },
{ "to": ObjectId("100000000000000000000000") }
]
}
},
{
$project: {
whom: {
$first: {
$filter: {
input: [ "$from", "$to" ],
as: "i",
cond: { $ne: [ "$$i", ObjectId("100000000000000000000000") ]
}
}
}
},
date: 1,
messageContent: 1,
to: 1,
from: 1
}
},
{
$sort: { "date": -1 }
},
{
$group: {
_id: "$whom",
date: { "$first": "$date" },
message: { "$first": "$messageContent" },
to: { "$first": "$to" },
from: { "$first": "$from" }
}
}
])
Result:
[
{
"_id": ObjectId("300000000000000000000000"),
"date": ISODate("2021-09-04T11:23:25.184Z"),
"message": "333"
},
{
"_id": ObjectId("200000000000000000000000"),
"date": ISODate("2021-09-03T11:23:25.184Z"),
"message": "222"
}
]
Example: mongoplayground

MongoDB $lookup on nested document, limit and count the retrieved data

I would like to get a count of all notifications that aren't read by an User ("A", "B", "C", etc) for each subRoom. Taking into account that it could be millions of notifications documents and hundreds of subrooms elements in Rooms Collections, i need to limit it. For that reason I've limited the $lookup for first 100 elements and then check if that notifications have been read or not by an User. I did it using documents (roomId) in $lookup but I cant do it using subdocuments (subRoom.id).
Notifications Collection is indexed using a Compound of (roomId: 1, timestamp: -1)
Notifications Collection: (id corresponds to notification id and roomId is the link to Rooms collection)
[{
"_id": "XXX",
"id": "1",
"read": ["A", "B", "C"],
"roomId": "c1d87a4c-231d-4cc8-8438-35cf21ed7fc5",
"content": "XXX",
"timestamp": { "$date": "2021-12-31T22:50:53.000Z" }
},{
"_id": "XXX",
"id": "2",
"read": ["C"],
"roomId": "c1d87a4c-231d-4cc8-8438-35cf21ed7fc5",
"content": "XXX",
"timestamp": { "$date": "2021-12-31T22:50:53.000Z" }
},
...
]
Rooms Collection:
[{
"_id": "XXX"
"subRoom": [{
"id": "c1d87a4c-231d-4cc8-8438-35cf21ed7fc5",
"image": "XXX",
"name": "XXX"
}, {
"id": "c2d5081e-0cf1-4e69-937d-be357da1d104",
"image": "XXX",
"name": "XXX"
}, {
"id": "530c2c02-26e8-441c-af39-c5232dfe1f73",
"image": "XXX",
"name": "XXX"
}],
"id": "453a6458-6545-4842-8946-05f49efea216",
"name": "XXX",
},
...
]
Code working using roomId instead subRoom.id:
{ $lookup: {
from: "notifications",
let: { "id": "$id" },
pipeline: [
{ $match: {
$expr:
{ $eq: [ "$roomId", "$$id" ] }
}},
{ $limit: 100},
{ $project: {_id: 0, read: 1}}
],
as: "messages"
}},
{ $project: {_id: 0, id: 1, notRead: {
$size: {
$filter: {
input: "$notifications",
cond: {
$not: {
$in: [
"A",
"$$this.read"
]
}
}
}
}
},
}
Code NOT WORKING using subRoom.id:
{ $lookup: {
from: "notifications",
let: { "id": "$subRoom.id" },
pipeline: [
{ $match: {
$expr:
{ $eq: [ "$roomId", "$$id" ] }
}},
{ $limit: 100},
{ $project: {_id: 0, read: 1}}
],
as: "messages"
}},
{
$addFields: {
items: {
$map: {
input: { $zip: { inputs: ["$subRoom", "$messages"] } },
in: { $mergeObjects: "$$this" },
},
},
},
},
.
. projection
.
Expected Result:
[{
"_id": "XXX"
"subRoom": [{
"id": "c1d87a4c-231d-4cc8-8438-35cf21ed7fc5",
"notRead": 50 //e.g
}, {
"id": "c2d5081e-0cf1-4e69-937d-be357da1d104",
"notRead": 35 //e.g
}, {
"id": "530c2c02-26e8-441c-af39-c5232dfe1f73",
"image": "XXX",
"notRead": 5 //e.g
}],
"id": "453a6458-6545-4842-8946-05f49efea216",
"name": "XXX",
},
...
]
Finally and very importantly, I want an scalable solution that can be done with big data.
Thank you very much in advance.
$unwind deconstruct subRoom array with preserve null and empty array property
$lookup with notification collection using pipeline, let to pass id to pipeline, check condition for roomId and user should not read notification
$group by null and count total unread notifications
$addFields to get count to notifications using $sum
$group by _id and reconstruct the subRoom array with required fields in result
db.rooms.aggregate([
{
$unwind: {
path: "$subRoom",
preserveNullAndEmptyArrays: true
}
},
{
$lookup: {
from: "nitifications",
let: { id: "$subRoom.id" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $eq: ["$$id", "$roomId"] } },
{ read: { $ne: "A" } }
]
}
},
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
],
as: "subRoom.notRead"
}
},
{
$addFields: {
"subRoom.notRead": { $sum: "$subRoom.notRead.count" }
}
},
{
$group: {
_id: "$_id",
name: { $first: "$name" },
id: { $first: "$id" },
subRoom: { $push: "$subRoom" }
}
}
])
Playground
Second option without using $unwind stage,
$lookup with notification collection using pipeline, let to pass id to pipeline, check condition for roomId and user should not read notification
$group by null and count total unread notifications
$map to iterate loop of subRoom array
$filter to iterate loop of return result from lookup notifications count and get current subRoom document
$let to declare a variable n and assign above filtered result to it and return $sum from count
$mergeObjects to merge current object of subRoom and new field notRead
db.rooms.aggregate([
{
$lookup: {
from: "nitifications",
let: { id: "$subRoom.id" },
pipeline: [
{
$match: {
$and: [
{ $expr: { $in: ["$roomId", "$$id"] } },
{ read: { $ne: "A" } }
]
}
},
{
$group: {
_id: "$roomId",
count: { $sum: 1 }
}
}
],
as: "notRead"
}
},
{
$project: {
id: 1,
name: 1,
subRoom: {
$map: {
input: "$subRoom",
as: "s",
in: {
$mergeObjects: [
"$$s",
{
notRead: {
$let: {
vars: {
n: {
$filter: {
input: "$notRead",
cond: { $eq: ["$$this._id", "$$s.id"] }
}
}
},
in: { $sum: "$$n.count" }
}
}
}
]
}
}
}
}
}
])
Playground

Mongodb aggregation lookup to add field in each array with condition

I have 3 collections.
User:
{
"_id":ObjectId("60a495cdd4ba8b122899d415"),
"email":"br9#gmail.com",
"username":"borhan"
}
Panel:
{
"_id": ObjectId("60a495cdd4ba8b122899d417"),
"name": "borhan",
"users": [
{
"role": "admin",
"joined": "2021-05-19T04:35:47.474Z",
"status": "active",
"_id": ObjectId("60a495cdd4ba8b122899d418"),
"user": ObjectId("60a495cdd4ba8b122899d415")
},
{
"role": "member",
"joined": "2021-05-19T04:35:47.474Z",
"status": "active",
"_id": ObjectId("60a49600d4ba8b122899d41a"),
"user": ObjectId("60a34e167958972d7ce6f966")
}
],
}
Team:
{
"_id":ObjectId("60a495e0d4ba8b122899d419"),
"title":"New Teams",
"users":[
ObjectId("60a495cdd4ba8b122899d415")
],
"panel":ObjectId("60a495cdd4ba8b122899d417")
}
I want to receive a output from querying Panel colllection just like this:
{
"_id": ObjectId("60a495cdd4ba8b122899d417"),
"name": "borhan",
"users": [
{
"role": "admin",
"joined": "2021-05-19T04:35:47.474Z",
"status": "active",
"_id": ObjectId("60a495cdd4ba8b122899d418"),
"user": ObjectId("60a495cdd4ba8b122899d415"),
"teams":[
{
"_id":ObjectId("60a495e0d4ba8b122899d419"),
"title":"New Teams",
"users":[
ObjectId("60a495cdd4ba8b122899d415")
],
"panel":ObjectId("60a495cdd4ba8b122899d417")
}
]
},
{
"role": "member",
"joined": "2021-05-19T04:35:47.474Z",
"status": "active",
"_id": ObjectId("60a49600d4ba8b122899d41a"),
"user": ObjectId("60a34e167958972d7ce6f966")
}
],
}
I mean i want to add teams field (which is array of teams that user is existed on it) to each user in Panel collection
Here is my match query in mongoose to select specific panel:
panel_model.aggregate([
{
$match: {
users: {
$elemMatch: {user: ObjectId("60a495cdd4ba8b122899d415"), role:"admin"}
}
}
},
])
Is it possible to get my output with $lookup or $addFields aggregations?
You need to join all three collections,
$unwind to deconstruct the array
$lookup there are two kind of lookups which help to join collections. First I used Multiple-join-conditions-with--lookup, and I used standrad lookup to join Users and Teams collections.
$match to match the user's id
$expr - when you use $match inside lookup, u must use it.
$set to add new fields
$group to we already destructed using $unwind. No we need to restructure it
here is the code
db.Panel.aggregate([
{ $unwind: "$users" },
{
"$lookup": {
"from": "User",
"let": { uId: "$users.user" },
"pipeline": [
{
$match: {
$expr: {
$eq: [ "$_id", "$$uId" ]
}
}
},
{
"$lookup": {
"from": "Team",
"localField": "_id",
"foreignField": "users",
"as": "teams"
}
}
],
"as": "users.join"
}
},
{
"$set": {
"users.getFirstElem": {
"$arrayElemAt": [ "$users.join", 0 ]
}
}
},
{
$set: {
"users.teams": "$users.getFirstElem.teams",
"users.join": "$$REMOVE",
"users.getFirstElem": "$$REMOVE"
}
},
{
"$group": {
"_id": "$_id",
"name": { "$first": "name" },
"users": { $push: "$users" }
}
}
])
Working Mongo playground
Note : Hope the panel and user collections are in 1-1 relationship. Otherwise let me know

Grouping in MongoDb using aggregate

I am a beginner to MongoDB and I found the Aggregate function hard to understand.
I read many topics and tried many things, however I couldn't get the results I am looking for.
Actually, I have two schema as:
1) Faculty.js
const FacultySchema = new Schema({
name: {
type: String,
required: true
}
});
2) Semester.js
const SemesterSchema = new Schema({
name: {
type: String,
required: true
},
faculty: {
type: Schema.Types.ObjectId,
ref: 'faculties'
}
});
Semester collection
[
{
"_id": ObjectId("5bf82da745209d0d48a91b62"),
"name": "1st Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c3f945209d0d48a91b63"),
"name": "2nd Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c3fe45209d0d48a91b64"),
"name": "3rd Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
},
{
"_id": ObjectId("5bf8c40345209d0d48a91b65"),
"name": "4th Semester",
"faculty": ObjectId("5bf7f39a1972dd0b6c74de7d"),
"__v": 0
}
]
What I want to group is all those semesters as an array having same faculty id in one place.
Something like:
[
{faculty: "BBA", semesters: ['first', 'second', 'third']},
{faculty: "BCA", semesters: ['first', 'second', 'third']}
];
How can I achieve this??
You can use $group aggregation to first find the distinct faculties and then $lookup to get the names of the faculties from the Faculties collection
Semester.aggregate([
{ "$group": {
"_id": "$faculty",
"semesters": { "$push": "$name" }
}},
{ "$lookup": {
"from": "faculties",
"let": { "facultyId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$facultyId"] }}}
],
"as": "faculty"
}},
{ "$project": {
"semesters": 1, "faculty": { "$arrayElemAt": ["$faculty.name", 0] }
}}
])
Or you can use $lookup first and then $grouping the distinct names
Semester.aggregate([
{ "$lookup": {
"from": "Faculty",
"let": { "facultyId": "$_id" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$_id", "$$facultyId"] }}}
],
"as": "faculty"
}},
{ "$unwind": "$faculty" },
{ "$group": {
"_id": "$faculty.name",
"semesters": { "$push": "$name" }
}},
{ "$project": {
"semesters": 1, "faculty": "$_id", "_id": 0 }
}}
])