mongodb aggregation: average and sorting - node.js

I am quite a newbie and have a problem for mongodb aggregation. I used mongoose.
var SubjectScore = new Schema({
name: {type:String, required:true}, //math, science, history, ...
score: {type:Number, required:true } // 95, 85, 77,....
});
var Subject = new Schema({
year: Number, //2012, 2013, 2014
subjectScore : [SubjectScore]
});
var StudentSchema = new Schema({
name: String,
subject: [Subject], //array length varies for each student
profile: String,
});
So, Input data is like this.
{ _id: 54c921aaa7918d4e4a8c7e51,
name: John,
profile: "He is nice",
subject: [{ year: 2010,
subjectScore: [{ name:"history" score:66},
{ name:"math", score:65},
{ name:"science", score:87}] }]
},{ year: 2011,
subjectScore: [{ name:"history" score:75},
{ name:"math", score:61},
{ name:"science", score:92}] }]
},{ year: 2012,
subjectScore: [{ name:"history" score:83},
{ name:"math", score:82},
{ name:"science", score:86}] }]
},{ year: 2013,
subjectScore: [{ name:"history" score:77},
{ name:"math", score:99},
{ name:"science", score:71}] }]
}]
}
The final result I want to get is like follows.
[
{ _id: "54c921aaa7918d4e4a8c7e51",
name: "John"
profile: "He is nice",
avgScore: [
{name: "math", score: 77},
{name:"history", score:78},
{name:"science", score:86} ]
totalAvg: 82
},
{ _id: "54c921aaa7918d4e4a8c7e5b",
name: "Mary"
profile: "She is kind",
avgScore: [
{name: "math", score: 67},
{name:"history", score:99},
{name:"science", score:96} ]
totalAvg: 82
},
{ _id: "54c921aaa7918d4e4a8c7e56",
name: "Jane"
profile: "She is smart",
avgScore: [
{name: "math", score: 99},
{name:"history", score:99},
{name:"science", score:99} ],
totalAvg: 99
}
..... // 7 more student for first page result
]
I tried following but I couldn't get the field of name, profile. So I needed additional query for getting name&profile fields and sorting again.
{$project:{subject:1}},
{$unwind:"$subject"},
{$unwind:"$subject.subjectScore"},
{$group:{_id:{studentId:"$_id", subjectName:"$subject.subjectScore.name"},
avgScore:{$avg: "$subject.subjectScore.score"}}},
{$group:{_id:"$_id.studentId",
avgScore:{$push: {name:"$_id.subjectName", score:"$avgScore"}},
totalAvg:{$avg:"$avgScore"}}},
{$sort:{totalAvg:-1}},
{$limit:10} // for first page (students per page : 10)
I want know how to keep the fields which is not necessary for aggregation but need to show as result. If it's not possible, do I need additional queries for result I want?
Are there another way to get this result considering performance?
I've already put a few days for this job and googling, but didn't get an answer. Help me please.
Thank you.

You are losing the information in your first operation because the $project is only sending along the value of the subject key.
The operation to keep the value of fields unaffected by aggregation calculations (name and profile) is with $first. It will just grab the first value of that field which should be the same as the rest of them.
{ $unwind: "$subject" },
{ $unwind: "$subject.subjectScore" },
{ $group: { _id: { sId: "$_id", subjectName: "$subject.subjectScore.name" },
avgScore: { $avg: "$subject.subjectScore.score" },
name: { $first: "$name" },
profile: { $first: "$profile" } } },
{ $group: { _id: "$_id.sId",
name: { $first: "$name" },
profile: { $first: "$profile" },
avgScore: { $push: { name: "$_id.subjectName", score: "$avgScore" } },
totalAvg: { $avg: "$avgScore" } } }
{ $sort: { totalAvg: -1 } }, // this is sorting desc, but sample is asc?
{ $limit: 10 }

Related

How to calculates through mongodb aggregation which days employee is absent if we have only data of present days

in my mongodb database when employee marked attendance it will create document in the database collection but if employee is not marking attendance no records is creating i want to find monthly attendance of employee on which days he is not marking the attendance and which days he marked attendance basically i want to create a monthly timesheet of employee
here is my mongoose model
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const attendanceSchema = mongoose.Schema(
{
date: {
type: Date,
},
check_in: {
type: Date,
},
check_in_remarks: {
type: String,
},
late_time: {
type: String,
},
check_out: {
type: Date,
},
check_out_remarks: {
type: String,
},
early_off: {
type: String,
},
over_time: {
type: String,
},
totalHours: {
type: Number,
},
status: {
type: String,
enum: ["In","late", "half-day", "absent", "present"],
},
user: {
type: Schema.Types.ObjectId,
ref: "Employee",
},
},
{
timestamps: true,
}
);
module.exports = mongoose.model("Attendance", attendanceSchema);
and here is my document details
{
"_id": "63904934763bb94e677e6692",
"date": "Wed Dec 07 2022 05:00:00 GMT+0500 (Pakistan Standard Time)",
"check_in": "2022-12-07T10:07:00.000Z",
"check_in_remarks": "",
"late_time": "00:07:00",
"status": "present",
"user": "638869649c443988469c151f",
"createdAt": "2022-12-07T08:05:08.992Z",
"updatedAt": "2022-12-07T12:03:53.457Z",
"__v": 0,
"check_out": "2022-12-07T18:07:00.000Z",
"check_out_remarks": "",
"early_off": "00:53:00",
"over_time": "",
"totalHours": 28800
}
Keeping date in string maybe not the best option , but if you dont have option to change the schema this may give you some ideas:
db.collection.aggregate([
{
$match: {
user: "testUser",
check_in: {
$regex: "^2022-12"
}
}
},
{
"$project": {
user: 1,
"date": {
"$toInt": {
"$substrCP": [
"$check_in",
8,
2
]
}
}
}
},
{
$group: {
_id: "$user",
dates: {
$addToSet: {
day: "$date"
}
}
}
},
{
$project: {
dates: {
$map: {
input: {
$range: [
1,
31
]
},
as: "day",
in: {
$let: {
vars: {
dayIndex: {
"$indexOfArray": [
"$dates.day",
"$$day"
]
}
},
in: {
$cond: {
if: {
$ne: [
"$$dayIndex",
-1
]
},
then: {
day: "$$day",
presence: "Present"
},
else: {
day: "$$day",
presence: "Abs"
}
}
}
}
}
}
}
}
}
])
Explained:
Match the user + month
Extract date of month from the check_in string and convert to Int
Group/addToSet(to form unique array with days of presence) the dates where user checked in
$map present days with $range generated days list , $range list can be updated to range based on month ...
Playground

How to process the fetched data from mongoDB?

I want my order_total to be calculated from results after joining two different collections
I'm building an Food Ordering App. I've seperate model for item:"ItemModel"
Here's my orderSchema which is being followed while ordering the items. I've referred to the ItemModel in case of Item_Id
My OrderSchema:
const cartOrderSchema = new mongoose.Schema({
items: [
{
item_id: { type: mongoose.Schema.Types.ObjectId, ref: "itemModel" },
quantity: { type: Number, default: 1 },
},
],
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: "userModel",
},
date: {
type: Date,
default: new Date(),
},
order_total: {
type: Number,
default: 0,
},
delivery_address: {
type: String,
default: "GGn",
},
payment_type: {
type:String,
default:"Cash"
},
});
I'm using this query:
await req.user.populate({
path: "mycartOrder",
populate:{
path:"items.item_id"
}
});
res.status(200).send(req.user.mycartOrder);
& getting result as:
{
"order_total": 0,
"delivery_address": "GGn",
"payment_type": "Cash",
"_id": "6270d84d89e3be5f291ee923",
"items": [
{
"item_id": {
"item_type": "Veg",
"_id": "62614934693ffd8d8f7979e9",
"name": "Cheese Burger",
"cuisine_category": "Starters",
"price": 67,
"description": "Aloo patty enclosed within Bun Tikki with melted cheese",
"store_id": "626148ca693ffd8d8f7979e2",
"__v": 0
},
"quantity": 10,
"_id": "6270d84d89e3be5f291ee924"
},
{
"item_id": {
"item_type": "Veg",
"_id": "62614977693ffd8d8f7979ec",
"name": "Paneer Lababdar",
"cuisine_category": "Indian",
"price": 249,
"description": "Panner vegetable cooked in very rich Gravy",
"store_id": "626148ca693ffd8d8f7979e2",
"__v": 0
},
"quantity": 3,
"_id": "6270d84d89e3be5f291ee925"
}
],
"date": "2022-05-03T07:22:03.907Z",
},
What I want?
I want that this order_total should be calculated automatically when the query is executed by multiplying the item's price with quantity.
Please HELP!!
If your collection is called cart (I'm not familiar with node.js or mongoose), you can use the $unwind operator and unwind your items. Then use the $addFields operator to add a new field overwriting your order_total, with the value to be the multiplication of the price and quantity. Then you'll have to group them back together, using the $sum operator on all the order_total fields, and then re-add all the previous fields..
db.cart.aggregate([
{
$unwind: "$items"
},
{
$addFields: {
"order_total": {
$multiply: [ "$items.quantity", "$items.item_id.price" ]
}
}
},
{
$group: {
_id: "$_id",
order_total: {$sum: "$order_total"},
delivery_address: { $first: "$delivery_address" },
payment_type: { $first: "$payment_type" },
items: { $push: "$items" },
date: {$first: "$date" }
}
}
])
You can skip the $addFields step and do the sum and multiply all in one step but I wanted to show both for clarity:
db.cart.aggregate([
{
$unwind: "$items"
},
{
$group: {
_id: "$_id",
order_total: {$sum: {$multiply: [ "$items.quantity", "$items.item_id.price" ]}},
delivery_address: { $first: "$delivery_address" },
payment_type: { $first: "$payment_type" },
items: { $push: "$items" },
date: {$first: "$date" }
}
}
])
You might need a match stage if you only want one specific cart.

How we can flatten lookup collection array of objects in mongodb?

Given 3 collections, I want to generate a single output containing an array of the selected data. Is it possible to do so?
The collections:
courses:
{
_id: ObjectId("623f0caa35e835e56639ecbf"),
course_id: "ff08780a-b72b-4cb5-948a-7bc34023d750",
name: "MERN Stack Zero To Hero with Full Project",
description: "Description ofCourse goes here",
cost_price: 1999.99,
created_by: "a747e664-92f0-4607-b467-5eab187db0e8",
updatedAt: ISODate("2022-03-27T08:56:14.518+0000"),
createdAt: ISODate("2022-03-26T12:52:58.508+0000"),
__v: NumberInt(0),
}
sections:
{
_id: ObjectId("624026a1866d58dcb644e66a"),
section_id: "1f0c5199-f3b5-47b5-8137-64341a4d17c9",
course_id: "ff08780a-b72b-4cb5-948a-7bc34023d750",
name: "This is Section-1",
createdAt: ISODate("2022-03-27T08:56:01.399+0000"),
updatedAt: ISODate("2022-03-27T08:57:39.088+0000"),
__v: NumberInt(0),
}
contents:
{
_id: ObjectId("624026f5866d58dcb644e686"),
content_id: "1ade4f4f-8ecd-46ac-9263-9be2e68f9c3a",
section_id: "1f0c5199-f3b5-47b5-8137-64341a4d17c9",
name: "This is first content-1",
content_url: "url of the content",
createdAt: ISODate("2022-03-27T08:57:25.977+0000"),
updatedAt: ISODate("2022-03-27T08:57:25.977+0000"),
__v: NumberInt(0),
}
Sections collections hold the courses id and contents collections hold the sectionId
My desired output is:
{
results:[
{id:7556922,title:"This is Section-1 Title",description:"this section description"},
{id:31559070,title:"Section 1 Content title content",description:"",},
{id:31559072,title:"Section 1 content title content",description:""},
{id:7556922,title:"This is Section-2 Title",description:"this section description"},
{id:31559070,title:"Section 2 Content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",},
{id:31559072,title:"Section 2 content title content",description:"",}
]
}
This is a piece of code I wrote up to now. Sadly it's not complete and I need help to achieve the desired result:
db.courses.aggregate([
{
$lookup: {
from: "sections",
let: { course_id: "$course_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$course_id", "$$course_id"] } } },
{
$lookup: {
from: "contents",
let: { section_id: "$section_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$section_id", "$$section_id"] } } },
],
as: "contents",
},
},
],
as: "sections",
},
},
{ $unwind: "$sections" },
]);
Any help would be very much appreciated, Thanks you very much in advance.

How to find Mongoose data recursively?

I am newbie in MEANJS and i have a problem i.e, there are collection called employee and have multiple documents with their boss field. Now i want get all employees with their lower level.
For example:-
1) {_id:ObjectId('587dcd3edca5f235f862fdfd'), name:'John'} //he doesn't have boss
2) {_id:ObjectId('587dcd3edca5f235f86dddew'), name: 'Jimmy', 'boss': ObjectId('587dcd3edca5f235f862fdfd')} //john is boss
3) {_id:ObjectId('587dcd3edca5f235f863ew'), name: 'David', 'boss': ObjectId('587dcd3edca5f235f86dddew')} //john,Jimmy are bosses
4) {_id:ObjectId('587dcd3edca5f235f86qwa'), name: 'Dyan', 'boss': ObjectId('587dcd3edca5f235f86dddew')} //john,Jimmy,David are bosses
5) {_id:ObjectId('587dcd3edca5f235f8ew32'), name:'Jack', 'boss': ObjectId('587dcd3edca5f235f862fdfd')} //john is boss
6) {_id:ObjectId('587dcd3edca5f2wsw23rlot'), name: 'Loren', 'boss':ObjectId('587dcd3edca5f235f8ew32')} //john,Jack is boss
If we take
Jonh then output will ['Jimmy','Jack','David','Dyan','Loren']
Jack then output will ['Loren']
Here is my try code:-
getBosses(user._id)
function getBosses(id){
User.find({boss:id})
.exec(function(err,users){
if(err)
return console.log(err);
//How handle here 'users' array
//for something getBosses call recursively
})
}
As far as I understood you need to find all subordinates of that people. I think the best way to do it is using $graphLookup.
db.bosses.insertMany([
{ _id: "587dcd3edca5f235f862fdfd", name: "John" },
{
_id: "587dcd3edca5f235f86dddew",
name: "Jimmy",
boss: "587dcd3edca5f235f862fdfd",
},
{
_id: "587dcd3edca5f235f863ew",
name: "David",
boss: "587dcd3edca5f235f86dddew",
},
{
_id: "587dcd3edca5f235f86qwa",
name: "Dyan",
boss: "587dcd3edca5f235f86dddew",
},
{
_id: "587dcd3edca5f235f8ew32",
name: "Jack",
boss: "587dcd3edca5f235f862fdfd",
},
{
_id: "587dcd3edca5f2wsw23rlot",
name: "Loren",
boss: "587dcd3edca5f235f8ew32",
},
]);
db.bosses.aggregate([
{
$graphLookup: {
from: "bosses",
startWith: "$_id",
connectFromField: "_id",
connectToField: "boss",
as: "subordinates",
},
},
{
$project: {
_id: false,
name: true,
subordinates: {
$reduce: {
input: "$subordinates",
initialValue: "",
in: { $concat: ["$$value", ", ", "$$this.name"] },
},
},
},
},
{
$project: {
name: true,
subordinates: { $substrBytes: ["$subordinates", 2, -1] },
},
},
]);
The result of the last one is:
[
{ name: 'John', subordinates: 'David, Dyan, Loren, Jack, Jimmy' },
{ name: 'Jimmy', subordinates: 'David, Dyan' },
{ name: 'David', subordinates: '' },
{ name: 'Dyan', subordinates: '' },
{ name: 'Jack', subordinates: 'Loren' },
{ name: 'Loren', subordinates: '' }
]
The most important thing is $graphLookup stage of the aggregate pipeline. Last two $project stages is just response formatting - return only name and subordinates as string field with comma separated names.
To get data for a specific person you can use $match stage before $graphLookup like that:
db.bosses.aggregate([
{ $match: { name: "John" } },
{
$graphLookup: ...

Finding in Mongoose with specific parametres

I have a database in Mongodb filled with profiles with different skills.
Here's an example of one profile.
{_id: 570b8b5afdcaf27c24a0a837,
identifier: 'Mr X',
url: 'https://MRX.com',
email: 'Mrx#gmail.com'
skills:
[ { _id: 570b8b5afdcaf27c24a0a858, title: 'Java', number: '74' },
{ _id: 570b8b5afdcaf27c24a0a857, title: 'Linux', number: '48' },
{ _id: 570b8b5afdcaf27c24a0a856, title: 'C++', number: '43' },
{ _id: 570b8b5afdcaf27c24a0a855, title: 'SQL', number: '34' },
{ _id: 570b8b5afdcaf27c24a0a854, title: 'XML', number: '28' },
{ _id: 570b8b5afdcaf27c24a0a853, title: 'MySQL', number: '23' },
{ _id: 570b8b5afdcaf27c24a0a852, title: 'C', number: '22' },
{ _id: 570b8b5afdcaf27c24a0a851,
title: 'Java Enterprise Edition',
number: '18' }]
}
My question: is there a query in mongoose where I can find a profile who has linux in his skills but also the number of that linux skill is greater than 40?
I tried with something like this in the finding option :
var x = {
'skills.title': 'Linux',
'skills.number': {
$gt: 40
},
}
but it doesn't work,the program finds the Linux skill but number 40 is not associated with Linux.
So is there a query to solve my problem?
Use $elemMatch:
db.yourCollection.find(
{ skills: { $elemMatch: { title: "Linux", number: { $gte: 40 } } } }
)
Taken from the docs:
The $elemMatch operator matches documents that contain an array field
with at least one element that matches all the specified query
criteria.
In mongoose you should do something like this:
var myCol = require('.myModel');
myCol.aggregate(
[
{$match:
{
title: "Linux",
number: { $gte: 40 }
}
}
], function (err, result) {
if (err) {
console.log(err);
} else {
console.log(result);
}
});

Resources