aggregate operation coming as null in mongodb - mongodb

Inventors
.aggregate([{
$match: filter
},
{
$group: {
"_id": {
"store_id": "$store_id"
},
stockAmount: {
$sum: {
$multiply: ["$intProductQty", "$dblMRP"]
}
},
storeValue: {
$sum: "$intProductQty"
},
}
},
])
.exec(function(err, stock) {
return res.send(stock);
});
schema
{
"store_id" : "BST000433",
"strProductCode" : "9000000064775",
"dblMRP" : 25,
"intProductQty" : 1,
}
I initailized these fields(intProductQty, dblMRP, strPurchasePrice) as integer. But when I execute above command, I'm getting that three values(stockAmount, purchaseAmount, storeValue) as null.

If it is still possible that some of those values are not set, you could check if they are null with $ifNull and set them to 0 for those calculations in a $project step after the $match:
$project: {
intProductQty: { $ifNull: [ "$intProductQty", 0 ] },
dblMRP: { $ifNull: [ "$dblMRP", 0 ] },
strPurchasePrice: { $ifNull: [ "$strPurchasePrice", 0 ] }
},
Also, I guess it's not your case, but you could filter out those that are not numeric with $type:
$match: {
intProductQty: { $type: "number" },
dblMRP: { $type: "number" },
strPurchasePrice: { $type: "number" }
},

Related

Aggregation error: $arrayElemAt's first argument must be an array, but is object

I'm trying to aggregate a collection in mongo using the following pipeline:
const results = await Price.aggregate([
{ $match: { date: today } },
{ $unwind: '$points' },
{ $match: { 'points.time': { $gte: start, $lte: now } } },
{ $sort: { 'points.time': 1 } },
{ $project: {
'high': { $max: '$points.price' },
'low': { $min: '$points.price' },
'open': { $arrayElemAt: ['$points', 0] },
'close': { $arrayElemAt: ['$points', -1] }
} }
])
However the $arrayElemAt operator isn't working preseumably because one of the preceding stages ($unwind I believe) converts the array of points I have in my documents to an object. How can I fix this?
Example document:
{
"_id" : ObjectId("5c93ac3ab89045027259a23f"),
"date" : ISODate("2019-03-21T00:00:00Z"),
"symbol" : "CC6P",
"points" : [
{
"_id" : ObjectId("5c93ac3ab89045027259a244"),
"volume" : 553,
"time" : ISODate("2019-03-21T09:35:34.239Z"),
"price" : 71
},
{
"_id" : ObjectId("5c93ac3ab89045027259a243"),
"volume" : 1736,
"time" : ISODate("2019-03-21T09:57:34.239Z"),
"price" : 49
},
....
],
My expected result is an array of objects where the points that should be passed to the project stage should be points in the specified range in the second $match. I tried combining the two $match stages and removing the $unwind stage and the error is gone however the time range isn't being applied
I believe you are missing a $group stage to rollback your points array
const results = await Price.aggregate([
{ "$match": { "date": today } },
{ "$unwind": "$points" },
{ "$match": { "points.time": { "$gte": start, "$lte": now } } },
{ "$sort": { "points.time": 1 } },
{ "$group": {
"_id": "$_id",
"points": { "$push": "$points" },
"date": { "$first": "$date" },
"symbol": { "$first": "$symbol" }
}},
{ "$project": {
"high": { "$max": "$points.price" },
"low": { "$min": "$points.price" },
"open": { "$arrayElemAt": ["$points", 0] },
"close": { "$arrayElemAt": ["$points", -1] }
}}
])

Adding up values from array elements in MongoDB

I have done some aggregation to arrive at the below document structure for my given data:
{
"_id" : "test",
"NoOfQuestions" : 3.0,
"info" : [
{
"AnswerrCount" : 3
},
{
"AnswerrCount" : 3
},
{
"AnswerrCount" : 2
}
]
}
However, I am trying to add up all the values in the AnswerrCount column. So from the above example, I want another column that says TotalAnswers:8, (3+3+2) and then eventually have a from using the NoOfQuestions, FinalTotal:11, (8+3)
You can use $sum aggregation to add array values
db.collection.aggregate([
{ "$addFields": {
"TotalAnswers": {
"$sum": "$info.AnswerrCount"
},
"FinalTotal": {
"$add": [{ "$sum": "$info.AnswerrCount" }, "$NoOfQuestions"]
}
}}
])
db.collection.aggregate([{
$unwind: "$info"
}, {
$group: {
_id: null,
TotalAnswers: {
$sum: '$info.AnswerrCount'
},
doc: {
$first: '$$CURRENT'
}
}
}, {
$project: {
TotalAnswers: 1,
FinalTotal: {
'$add': ['$TotalAnswers', '$doc.NoOfQuestions']
},
_id: 0
}
}])

Need to generate a structured document out of MongoDB collection

We have a MongoDB collection like this:
{
a : string, nullable
b : string, nullable
c : boolean, not nullable
d : string, nullable
e : number, nullable
}
out of which we need the result like this:
{
ab : {
a: <count of a where a is not blank or null>,
b: <count of b where b is not blank or null>
},
c : {
true: <count of c where c true>,
false: <count of c where c false>
},
d : [<all distinct/unique values of d],
e : {
average : <average value of e>,
min : <minimum value of e>,
max : <maximum value of e>
}
}
We do not wish to fire multiple find queries and manipulate the result in-memory to bring up the results.
How do we achieve this using MongoDB's queries only? Any suggestions will be appreciated
You need to run multiple pipelines and then combine results. That's possible with $facet operator. Try below query:
db.col.aggregate([
{
$facet: {
q1: [
{ $match: { a: { $exists: true, $ne: null } } },
{ $count: "total" }
],
q2: [
{ $match: { b: { $exists: true, $ne: null } } },
{ $count: "total" }
],
q3: [
{ $match: { c: true } },
{ $count: "total" }
],
q4: [
{ $match: { c: false } },
{ $count: "total" }
],
q5: [
{
$group: {
_id: null,
unique: { $addToSet: "$d" }
}
}
],
q6: [
{
$group: {
_id: null,
average: { $avg: "$e" },
min: { $min: "$e" },
max: { $max: "$e" },
}
}
],
}
},
{
$project: {
q1: { $arrayElemAt: [ "$q1", 0 ] },
q2: { $arrayElemAt: [ "$q2", 0 ] },
q3: { $arrayElemAt: [ "$q3", 0 ] },
q4: { $arrayElemAt: [ "$q4", 0 ] },
q5: { $arrayElemAt: [ "$q5", 0 ] },
q6: { $arrayElemAt: [ "$q6", 0 ] }
}
},
{
$project: {
"ab.a": { $ifNull: [ "$q1.total", 0 ] },
"ab.b": { $ifNull: [ "$q2.total", 0 ] },
"c.true": { $ifNull: [ "$q3.total", 0 ] },
"c.false": { $ifNull: [ "$q4.total", 0 ] },
d: "$q5.unique",
"e.average": "$q6.average",
"e.min": "$q6.min",
"e.max": "$q6.max",
}
}
])
So q1-q6 are just separate aggregation pipelines. Each of them returns an array of results which can be converted to separate subdocuments using $arrayElemAt. Then you can use simple $project to reshape that into final result. Using $addToSet to get unique values for d and $ifNull to replace those counts when there's no value with default 0.

Compare 2 count aggregations

I have a collection in MongoDB that looks something like the following:
{ "_id" : 1, "type" : "start", userid: "101", placementid: 1 }
{ "_id" : 2, "type" : "start", userid: "101", placementid: 2 }
{ "_id" : 3, "type" : "start", userid: "101", placementid: 3 }
{ "_id" : 4, "type" : "end", userid: "101", placementid: 1 }
{ "_id" : 5, "type" : "end", userid: "101", placementid: 2 }
and I want to group results by userid then placementid and then count the types of "start" and "end", but only when the two counts are different. In this particular example I would want to get placementid: 3 because when grouped and counted this is the only case where the counts don't match.
I've written a query that gets the 2 counts and the grouping but I can't do the filtering when counts don't match. This is my query:
db.getCollection('mycollection').aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$match: {
countStart: {$ne: "$countEnd"}
}
}
])
It seems like I'm using the match aggregation incorrectly because I'm seeing results where countStart and countEnd are the same.
{ "_id" : {"userid" : "101", "placementid" : "1"}, "countStart" : 1.0, "countEnd" : 1.0 }
{ "_id" : {"userid" : "101", "placementid" : "2"}, "countStart" : 1.0, "countEnd" : 1.0 }
{ "_id" : {"userid" : "101", "placementid" : "3"}, "countStart" : 1.0, "countEnd" : 0 }
Can anybody point into the right direction please?
To compare two fields inside $match stage you need $expr which is available in MongoDB 3.6:
db.myCollection.aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$match: {
$expr: { $ne: [ "$countStart", "$countEnd" ] }
}
}
])
If you're using older version of MongoDB you can use $redact:
db.myCollection.aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$redact: {
$cond: { if: { $ne: [ "$countStart", "$countEnd" ] }, then: "$$KEEP", else: "$$PRUNE" }
}
}
])
You run do the following pipeline to get this - no need to use $expr or $redact or anything special really:
db.mycollection.aggregate({
$group: {
_id: {
"userid": "$userid",
"placementid": "$placementid"
},
"sum": {
$sum: {
$cond: {
if: { $eq: [ "$type", "start" ] },
then: 1, // +1 for start
else: -1 // -1 for anything else
}
}
}
}
}, {
$match: {
"sum": { $ne: 0 } // only return the non matching-up ones
}
})

MongoDB: elemMatch match the last element in an array

I have the data like below:
{
"order_id" : 1234567,
"order_pay_time" : 1437373297,
"pay_info" : [
{
"pay_type" : 0,
"pay_time" : 1437369046
},
{
"pay_type" : 0,
"pay_time" : 1437369123
},
{
"pay_type" : 0,
"pay_time" : 1437369348
}
]}
what I want to get is the last payment is of type 1, but $elemMatch just match the list where pay_type:1 exists, how can I match the orders which last payment is of "pay_type" : 1
You can use aggregation to get expected output. The query will be like following:
db.collection.aggregate({
$unwind: "$pay_info"
}, {
$match: {
"pay_info.pay_type": 1
}
}, {
$group: {
_id: "$_id",
"pay_info": {
$push: "$pay_info"
},
"order_id": {
$first: "$order_id"
},
"order_pay_time": {
$first: "$order_pay_time"
}
}
})
Moreover if you want latest pay_info.pay_time then you can sort it by descending order with limit 1, some what like following:
db.collection.aggregate({
$unwind: "$pay_info"
}, {
$match: {
"pay_info.pay_type": 1
}
}, {
$sort: {
"pay_info.pay_time": -1
}
}, {
$limit: 1
}, {
$group: {
_id: "$_id",
"pay_info": {
$push: "$pay_info"
},
"order_id": {
$first: "$order_id"
},
"order_pay_time": {
$first: "$order_pay_time"
}
}
})
Edit
Also you can use $redact to avoid $unwind like following:
db.collection.aggregate({
$match: {
"pay_info": {
$elemMatch: {
"pay_type": 1
}
}
}
}, {
$sort: {
"pay_info.pay_time": -1
}
}, {
$limit: 1
}, {
$redact: {
$cond: {
if: {
$eq: [{
"$ifNull": ["$pay_type", 1]
}, 1]
},
then: "$$DESCEND",
else: "$$PRUNE"
}
}
}).pretty()
Just found this thread for a similar problem I've had.
I ended up doing this, maybe that will be of interest to someone:
db.collection.find({
$where: function(){
return this.pay_info[this.pay_info.length-1].pay_type === 1
}
})