The docs are simple as:
[
{'id': '1', 'type': 'a', 'startedAt': '2017-06-11'},
{'id': '2', 'type': 'b', 'startedAt': ''},
{'id': '3', 'type': 'b', 'startedAt': '2017-06-11'}
]
And the expected aggregated result:
[
{'type': 'a', 'started': true, 'count': 1},
{'type': 'b', 'started': true, 'count': 1},
{'type': 'b', 'started': false, 'count': 1}
]
How to get above result with mongodb nodejs driver?
I've tried like below, but it didn't work ('started' was always null):
db.collection('docs').group(
{'type': '$type', 'started': {
$cond: [{$eq: ['$startedAt': '']}, false, true ]
}},
{},
{'total': 0},
'function(curr, result) {result.total++}'
)
You use .aggregate() here and not .group(), which is a different function altogether:
db.collection('docs').aggregate([
{ "$group": {
"_id": {
"type": "$type",
"started": {
"$gt": [ "$startedAt", "" ]
}
},
"count": { "$sum": 1 }
}}
],function(err, results) {
console.log(results);
})
The $gt operator returns true when the condition is met. In this case any content in a string is "greater than" an empty string.
If the field is actually "not present at all" then we can adapt with $ifNull. This gives a default have if the property does not actually exist, or otherwise evaluates to null.
db.collection('docs').aggregate([
{ "$group": {
"_id": {
"type": "$type",
"started": {
"$gt": [ { "$ifNull": [ "$startedAt", ""] }, "" ]
}
},
"count": { "$sum": 1 }
}}
],function(err, results) {
console.log(results);
})
This would produce:
{ "_id" : { "type" : "b", "started" : true }, "count" : 1 }
{ "_id" : { "type" : "b", "started" : false }, "count" : 1 }
{ "_id" : { "type" : "a", "started" : true }, "count" : 1 }
You can optionally $project afterwards to change the fields from being within _id in the results, but you really should not since this means an additional pass through results, when you can just as easily access the values anyway.
So just .map() on the result:
console.log(
results.map(function(r) {
return { type: r._id.type, started: r._id.started, count: r.count }
})
);
But with $project:
db.collection('docs').aggregate([
{ "$group": {
"_id": {
"type": "$type",
"started": {
"$gt": [ { "$ifNull": [ "$startedAt", ""] }, "" ]
}
},
"tcount": { "$sum": 1 }
}},
{ "$project": {
"_id": 0,
"type": "$_id.type",
"started": "$_id.started",
"count": "$tcount"
}}
],function(err, results) {
console.log(results);
})
Resulting in your desired format
{ "type" : "b", "started" : true, "count" : 1 }
{ "type" : "b", "started" : false, "count" : 1 }
{ "type" : "a", "started" : true, "count" : 1 }
For reference, the correct usage with .group() would be:
db.collection('docs').group(
function(doc) {
return {
"type": doc.type,
"started": (
(doc.hasOwnProperty('startedAt') ? doc.startedAt : "") > ""
)
}
},
[],
{ "count": 0 },
function(curr,result) {
result.count += 1
},
function(err,results) {
console.log(results);
}
);
Which returns:
[
{ "type" : "a", "started" : true, "count" : 1 },
{ "type" : "b", "started" : false, "count" : 1 },
{ "type" : "b", "started" : true, "count" : 1 }
]
But you really should no use that since .group() relies on JavaScript evaluation that runs much slower than what you can do with .aggregate()
Related
I'm trying to aggregate a set of transactions using the data set below and choose a winner in every grade. The winner is randomly chosen from within the grade.
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500a"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 32,
"ccTranId" : "HzP4B",
"firstName" : "Jason",
"lastName" : "Jones",
"grade" : "1",
"teacher" : "Smith, Bob",
"studentId" : 100 },
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500b"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 15,
"ccTranId" : "HzP4A",
"firstName" : "Joey",
"lastName" : "Jones",
"grade" : "1",
"teacher" : "Smith, Jane",
"studentId" : 200 },
{ "_id" : ObjectId("5ce6fb4b3d1be918e574500c"),
"eventId" : ObjectId("5ce2f540bf126322a6be559b"),
"donationAmt" : 25,
"ccTranId" : "HzP4D",
"firstName" : "Carrie",
"lastName" : "Jones",
"grade" : "2",
"teacher" : "Smith, Sally",
"studentId" : 300 }
I'm using this script to aggregate.
Donation.aggregate([
{
$match: {
eventId: mongoose.Types.ObjectId(eventId)
}
},
{
"$group": {
"_id": "$studentId",
"first": { "$first": "$firstName" },
"last": { "$first": "$lastName" },
"grade": { "$first": "$grade" },
"teacher": { "$first": "$teacher" }
}
},
{
"$group": {
"_id": "$grade",
"students": {
$push: '$$ROOT'
}
}
}
, { $sort: { _id: 1 } }
])
The output gives me this to work with. Then, I iterate through the each element and assign one of the students in the subdocument as winner.
The double group seems sloppy and it would be nice to execute an expression within a $project clause to randomly assign the winner.
Is there a cleaner way?
{
"_id":"1",
"students":[
{
"_id":100,
"first":"Jason",
"last":"Jones",
"grade":"1",
"teacher":"Smith, Bob"
},
{
"_id":200,
"first":"Joey",
"last":"Jones",
"grade":"1",
"teacher":"Smith, Jae"
}
]
},
{
"_id":"2",
students":[ .... ]
},
Random means that you need to get unpredictable results. The only operator that can help you in MongoDB is $sample. Unfortunately you can't sample arrays. All you can do is to apply filtering condition and then run { $sample: { size: 1 } } on that filtered data set:
db.col.aggregate([
{
$match: {
eventId: ObjectId("5ce2f540bf126322a6be559b"),
grade: "2"
}
},
{ $sample: { size: 1 } }
])
To make it a little bit more useful you can take advantage of $facet and run multiple samples for every grade in one query:
db.col.aggregate([
{
$match: {
eventId: ObjectId("5ce2f540bf126322a6be559b")
}
},
{
$facet: {
winner1: [
{ $match: { grade: "1" } },
{ $sample: { size: 1 } }
],
winner2: [
{ $match: { grade: "2" } },
{ $sample: { size: 1 } }
]
// other grades ...
}
}
])
I have a collection as follows
// collection: appointments
{
"_id" : ObjectId("5c50682b663e854a1c2d9401"),
"status" : "Pending",
"discount" : 0,
"removed" : false,
"services" : [
{
"_id" : ObjectId("5c505a29af3a655b98812ca7"),
"service" : ObjectId("5c505a12af3a655b98812ca5"),
"cost" : 200
},
{
"_id" : ObjectId("5c50691ab9081f53287d2354"),
"service" : ObjectId("5c5069a600ec0d7a1800aa73"),
"cost" : 200
}
],
"doctor" : ObjectId("5c5059b2af3a655b98812ca1"),
"patient" : ObjectId("5c5059e5af3a655b98812ca4"),
"date" : ISODate("2018-11-12T00:00:00.000+02:00"),
"clinic" : ObjectId("5c5059d8af3a655b98812ca3"),
"diagnosis" : [ ],
"rx" : [ ],
"labs" : [ ],
"scans" : [ ],
"__v" : 0
}
I'm trying to aggregate that collection, but i want to populate services.service as it's an object id
let appointments = await Appointment.aggregate([
{ $lookup: { from: 'services',localField: 'services.service',foreignField: '_id',as: 'services' } },
{ $project: {
'date': 1 ,
'status': 1 ,
'services': 1 ,
} },
{ $limit: Number(req.query.limit) },
{ $skip: Number(req.query.skip) }
]);
what i'm getting
"appointments": [
{
"_id": "5c50682b663e854a1c2d9401",
"status": "Pending",
"discount": 0,
"paidAmount": 0,
"services": [
{
"_id": "5c505a12af3a655b98812ca5",
"removed": false,
"name": "kashf",
"clinic": "5c5059d8af3a655b98812ca3",
"updatedAt": "2019-01-29T13:50:10.651Z",
"createdAt": "2019-01-29T13:50:10.651Z",
"__v": 0
},
{
"_id": "5c5069a600ec0d7a1800aa73",
"removed": false,
"name": "arza3",
"clinic": "5c5059d8af3a655b98812ca3",
"updatedAt": "2019-01-29T14:56:38.314Z",
"createdAt": "2019-01-29T14:56:38.314Z",
"__v": 0
}
],
"date": "2018-11-11T22:00:00.000Z"
}
]
so i lost the cost attribute, also the id of the object array
any solution for this ?
i tried unwinding the services, but it results in two appointments objects with the same id
I've got it
let appointments = await Appointment.aggregate([
{ $unwind: '$services' },
{ $lookup: { from: 'services',localField: 'services.service',foreignField: '_id',as: 'services.service' } },
{ $unwind: '$services.service' },
{
$group: {
'_id': '$_id',
'services': { $push: '$services' },
}
},
{ $project: {
'services': 1 ,
} },
]);
u can use multiple populate lik that
CHAR.findOneAndUpdate({id: info.id}, char, {upsert: true, new: true})
.populate({path : 'intels', populate : {path : 'intels', populate : {path : 'from'}}})
.populate({path : 'alts', populate : {path : 'alts', populate : {path : 'intels', populate : {path : 'intels.from'}}}})
I have the collection like below
{
"_id" : ObjectId("5b6538704ba0292b6c197770"),
"Name":"Name1",
"Status":"Good",
},
{
"_id" : ObjectId("5b6538704ba0292b6c197773"),
"Name":"Name2",
"Status":"Bad"
},
{
"_id" : ObjectId("5b6538704ba0292b6c197774"),
"Name":"Name3",
"Status":"Bad"
},{
"_id" : ObjectId("5b6538704ba0292b6c197774"),
"Name":"Name1",
"Status":"Bad"
},
{
"_id" : ObjectId("5b6538704ba0292b6c197775"),
"Name":"Name1",
"Status":"Good"
}
I have used the query to get the status wise count like below
db.Students.aggregate( [
{ $group: { _id: {
"Name": "$Name","Status":"$Status"}, StatusCount: { $sum: 1 } } }
, { "$project": { _id: 0, Name: "$_id.Name",Status : "$_id.Status", StatusCount:1 } }
] );
The result was
{
"Name":"Name1",
"StatusCount" : 2,
"Status" : "Good"
},
{
"Name":"Name2",
"StatusCount" : 1,
"Status" : "Bad"
}, {
"Name":"Name2",
"StatusCount" : 1,
"Status" : "Bad"
},
{
"Name":"Name1",
"StatusCount" : 1,
"Status" : "Bad"
}
The result what I am approaching is like
{
"Name":"Name1",
"Good" : 2,
"Bad" :1
},
{
"Name":"Name2",
"Good" : 0,
"Bad" :1
}
The result I am expecting to have the status of field names and count as its values. I have tried to do this but I could not make it happen. The status, for now, is only two like Good or Bad but may increase in real dataset.
By using the $arrayToObject operator and a final $replaceRoot pipeline step which has a $mergeObjects operator you will get your desired result.
You would need to run the following aggregate pipeline on MongoDB Server 3.4.4 or newer:
const pipeline = [
{ '$group': {
'_id': {
'Name': '$Name',
'Status': '$Status'
},
'StatusCount': { '$sum': 1 }
} },
{ '$group': {
'_id': '$_id.Name',
'counts': {
'$push': {
'k': '$_id.Status',
'v': '$StatusCount'
}
}
} },
{ '$replaceRoot': {
'newRoot': { '$mergeObjects': [
{ '$arrayToObject': '$counts' },
{ 'Name': '$_id' }
] }
} }
];
db.Students.aggregate(pipeline);
Here is my item model.
const itemSchema = new Schema({
name: String,
category: String,
occupied: [Number],
active: { type: Boolean, default: true },
});
I want to filter 'occupied' array. So I use aggregate and unwind 'occupied' field.
So I apply match query. And group by _id.
But if filtered 'occupied' array is empty, the item disappear.
Here is my code
Item.aggregate([
{ $match: {
active: true
}},
{ $unwind:
"$occupied",
},
{ $match: { $and: [
{ occupied: { $gte: 100 }},
{ occupied: { $lt: 200 }}
]}},
{ $group : {
_id: "$_id",
name: { $first: "$name"},
category: { $first: "$category"},
occupied: { $addToSet : "$occupied" }
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Here is example data set
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 95, 100, 145, 200 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
The result above query
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
}
]
and the result I want
[
{
"_id" : ObjectId("59c1bced987fa30b7421a3eb"),
"name" : "printer1",
"category" : "printer",
"occupied" : [ 100, 145 ],
"active" : true
},
{
"_id" : ObjectId("59c2dbed992fb91b7421b1ad"),
"name" : "printer2",
"category" : "printer",
"occupied" : [ ],
"active" : true
}
]
how could I do this??
Thanks in advance.
In the simplest form, you keep it simply by not using $unwind in the first place. Your conditions applied imply that you are looking for the "unique set" of matches to specific values.
For this you instead use $filter, and a "set operator" like $setUnion to reduce the input values to a "set" in the first place:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$filter": {
"input": { "$setUnion": [ "$occupied", []] },
"as": "o",
"cond": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
}
}
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
Both have been around since MongoDB v3, so it's pretty common practice to do things this way.
If for some reason you were still using MongoDB 2.6, then you could apply $map and $setDifference instead:
Item.aggregate([
{ "$match": { "active": true } },
{ "$project": {
"name": 1,
"category": 1,
"occupied": {
"$setDifference": [
{ "$map": {
"input": "$occupied",
"as": "o",
"in": {
"$cond": {
"if": {
"$and": [
{ "$gte": ["$$o", 100 ] },
{ "$lt": ["$$o", 200] }
]
},
"then": "$$o",
"else": false
}
}
}},
[false]
]
}
}}
], (err, items) => {
if (err) throw err;
return res.json({ data: items });
});
It's the same "unique set" result as pulling the array apart, filtering the items and putting it back together with $addToSet. The difference being that its far more efficient, and retains ( or produces ) an empty array without any issues.
Here's my collection [test].
{"_id" : "Test1", "enabled": "on", "value" : 10},
{"_id" : "Test2", "enabled": "on", "value" : 50},
{"_id" : "Test3", "enabled": "on", "value" : 10},
{"_id" : "Test4", "value" : 5},
{"_id" : "Test5", "value" : 2}
I would like to get all the total of the value and total value of the field with "enabled":"on" like these:
Desired result:
[ { _id: null,
totalValue: 77,
totalEnabled: 70
} ]
Here's what i have so far but no luck.
db.collection('test').aggregate({
$group: {
_id: null,
totalValue : {
$sum: "$value"
},
totalEnabled: $sum : {"enabled":{$exists:true}}
}
}, function(err, result) {
if (err) return console.dir(err)
console.log(result);
});
You were close but $exists doesn't function is aggregation and has a different function. What you were looking for is $cond
db.items.aggregate([
{$group: {
_id: null,
totalValue: {$sum: "$value"},
enabledValue: {$sum: {
$cond: [
// Condition to test
{$eq: ["$enabled", "on"] },
// True
"$value",
// False
0
]
}}
}}
])
The usage is to provide a different value depending on whether the condition is evaluated to true or false.