Optimize multiple "and" statements in mongo aggregate - mongodb

Is there a simpler way that would also improve the performance of this mongodb query. I know I am suppose to group the either one or the other but cant find any docs or example to help me out.
const facetQuery = { $facet: {
xCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'x' } ]}},
{ $count: "Created" },
],
xApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'x' }]}},
{ $count: "Approved" }
],
xDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'x' }]}},
{ $count: "Disapproved" }
],
yCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'y' }]}},
{ $count: "Created" },
],
yApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'y' }]}},
{ $count: "Approved" }
],
yDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'y' }]}},
{ $count: "Disapproved" }
],
zCreated: [
{ $match : { $and : [{ queueStatus: 'Created' }, { queueType: 'z' }]}},
{ $count: "Created" },
],
zApproved: [
{ $match : { $and : [{ queueStatus: 'Approved' }, { queueType: 'z' }]}},
{ $count: "Approved" }
],
zDisapproved: [
{ $match : { $and : [{ queueStatus: 'Disapproved' }, { queueType: 'z' }]}},
{ $count: "Disapproved" }
],
}};

Oh wow, instead of doing all these separate matches and count you can just dynamically $group on both status and type and then construct the object you need from that:
db.collection.aggregate([
{
$group: {
_id: {
type: "$queueType",
status: "$queueStatus"
},
ApprovedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Approved"
]
},
1,
0
]
}
},
CreatedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Created"
]
},
1,
0
]
}
},
DisapprovedCount: {
$sum: {
$cond: [
{
$eq: [
"$queueStatus",
"Disapproved"
]
},
1,
0
]
}
},
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: {
$arrayToObject: [
[
{
k: {
$concat: [
"$_id.type",
"$_id.status"
]
},
v: {
$switch: {
branches: [
{
case: {
$eq: [
"$_id.status",
"Approved"
]
},
then: "$ApprovedCount"
},
{
case: {
$eq: [
"$_id.status",
"Created"
]
},
then: "$CreatedCount"
},
{
case: {
$eq: [
"$_id.status",
"Disapproved"
]
},
then: "$DisapprovedCount"
},
]
}
}
}
]
]
}
}
}
}
])
Mongo Playground

Related

MongoDB: add element to an inner array of array with an object that contains field calculated on another field

I have this document:
{
"_id" : ObjectId("626c0440e1b4f9bb5568f542"),
"ap" : [
{
"ap_id" : ObjectId("000000000000000000000001"),
"shop_prices" : [
{
"shop_id" : ObjectId("000000000000000000000097"),
"price" : 102
}
]
}
],
"bc" : [
{
"bc_id" : ObjectId("000000000000000000000003"),
"price" : 102
},
{
"bc_id" : ObjectId("000000000000000000000004"),
"price" : 104
}
],
"stock_price" : 70
}
My need is to eventually add to ap.shop_prices an element if not exists with this structure:
{
"shop_id" : ObjectId("000000000000000000000096"),
"price" : 104
}
where the price is bc.price where bc.bc_id = ObjectId("000000000000000000000004")
This is my first (unsuccesfull) try:
updateMany(
{
"_id": {"$eq": ObjectId("626c0421e1b4f9bb5568f531")},
"ap":{
$elemMatch:{
"ap_id":{$in:[ObjectId("000000000000000000000001")]},
"shop_prices.shop_id":{$ne:ObjectId("000000000000000000000096")}
}
},
"bc.bc_id": ObjectId("000000000000000000000003")
},
[
{"$set":
{"ap.$.shop_prices":
{"$cond":
[{"$in": [ObjectId("000000000000000000000096"), "$ap.$.shop_prices.shop_id"]}, "$ap.$.shop_prices",
{"$concatArrays":
["$ap.$.shop_prices",
[{"shop_id": ObjectId("000000000000000000000096"), "price": ???}]
]
}
]
}
}
}
]
)
thanks in advance
You can do that:
finding the bc related to your request using the $project
using $map in the $set operator
This should be the solution:
db.getCollection('test').update({
"ap": {
$elemMatch: {
"ap_id":{$in:[ObjectId("000000000000000000000001")]},
"shop_prices.shop_id":{$ne:ObjectId("000000000000000000000096")}
}
},
"bc.bc_id": ObjectId("000000000000000000000004")
},
[
{
$project: {
ap: 1,
bc: 1,
stock_price: 1,
current_bc: {
$arrayElemAt: [ {
$filter: {
input: "$bc",
as: "curr_bc",
cond: {$eq: ["$$curr_bc.bc_id", ObjectId("000000000000000000000004")]}
}
}, 0 ]
}
}
},
{
$set: {
"ap": {
"$map": {
input: "$ap",
as: "current_ap",
in: {
$cond: [
{$eq: [ObjectId("000000000000000000000001"), "$$current_ap.ap_id"]},
{
"$mergeObjects": [
"$$current_ap",
{"shop_prices": {$concatArrays: ["$$current_ap.shop_prices", [{"shop_id": ObjectId("000000000000000000000096"), "price": "$current_bc.price"}]]}}
]
},
"$$current_ap"
]
}
}
}
}
}
])

mongodb lookup using multiple fields not working

I am trying to get some other information from titleInfo collection using siteId of regCodes collection
I have two collections
regCodes
{
"siteId" : "123A",
"registration_code" : "ABC",
"used_flag" : true,
"Allowed_Use" : 1,
"Remaining_Use" : 0,
"BatchId" : "SNGL",
"CodeDuration" : 180
}
titleInfo
{
"title" : "Principles of Microeconomics",
"product_form_detail" : "EPUB",
"final_binding_description" : "Ebook",
"vitalsource_enabled" : false,
"reading_line" : "with InQuizitive and Smartwork5",
"volume" : "",
"protected_content" : {
"ebookSiteIds" : [
"123A"
],
"studySpaceSiteIds" : [],
"iqSiteIds" : []
}
}
below query not working, getting 'regcodeData' as empty array.
using mongodb version 3.6.18
db.getCollection('regCodes').aggregate([
{
$match: {
registration_code: 'ABC'
}
},
{
$lookup: {
from: "titleInfo",
let: {
regcode_siteId: "$siteId"
},
pipeline: [
{
$match: {
$expr: {
$or: [
{
$eq: [
"$protected_content.ebookSiteIds",
"$$regcode_siteId"
]
},
{
$eq: [
"$protected_content.studySpaceSiteIds",
"$$regcode_siteId"
]
},
{
$eq: [
"$protected_content.iqSiteIds",
"$$regcode_siteId"
]
}
]
}
}
}
],
as: "regcodeData"
}
}
])
below query is working as expected
db.getCollection('titleInfo').find({
$or: [
{
"protected_content.ebookSiteIds": "123A"
},
{
"protected_content.studySpaceSiteIds": "123A"
},
{
"protected_content.iqSiteIds": "123A"
}
]
})
You just need to unwind the arrays, by using $unwind operator with preserveNullAndEmptyArrays option set to true.
Updated Query:
db.regCodes.aggregate([
{
$match: {
registration_code: "ABC"
}
},
{
$lookup: {
from: "titleInfo",
let: {
regcode_siteId: "$siteId"
},
pipeline: [
{
$unwind: {
path: "$protected_content.ebookSiteIds",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$protected_content.studySpaceSiteIds",
preserveNullAndEmptyArrays: true
}
},
{
$unwind: {
path: "$protected_content.iqSiteIds",
preserveNullAndEmptyArrays: true
}
},
{
$match: {
$expr: {
$or: [
{
$eq: [
"$protected_content.ebookSiteIds",
"$$regcode_siteId"
]
},
{
$eq: [
"$protected_content.studySpaceSiteIds",
"$$regcode_siteId"
]
},
{
$eq: [
"$protected_content.iqSiteIds",
"$$regcode_siteId"
]
}
]
}
}
}
],
as: "regcodeData"
}
}
])
MongoPlayGroundLink
My bad trying to match array with string
Answer is as below
db.getCollection('regCodes').aggregate([
{
$match: {
registration_code: 'ABC'
}
},
{
$lookup: {
from: "titleInfo",
let: {
regcode_siteId: "$siteId"
},
pipeline: [
{
$match: {
$expr: {
$or: [
{
$in: [
"$$regcode_siteId",
"$protected_content.ebookSiteIds"
]
},
{
$in: [
"$$regcode_siteId",
"$protected_content.studySpaceSiteIds"
]
},
{
$in: [
"$$regcode_siteId",
"$protected_content.iqSiteIds"
]
}
]
}
}
}
],
as: "regcodeData"
}
}
])

Count Both Outer and Inner embedded array in a single query

{
_id: ObjectId("5dbdacc28cffef0b94580dbd"),
"comments" : [
{
"_id" : ObjectId("5dbdacc78cffef0b94580dbf"),
"replies" : [
{
"_id" : ObjectId("5dbdacd78cffef0b94580dc0")
},
]
},
]
}
How to count the number of element in comments and sum with number of relies
My approach is do 2 query like this:
1. total elements of replies
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{ $unwind: "$comments",},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
2. count total elements of comments
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
Then sum up both, do we have any better solution to write the query like return the sum of of total element comments + replies
You can use $reduce and $concatArrays to "merge" an inner "array of arrays" into a single list and measure the $size of that. Then simply $add the two results together:
db.posts.aggregate([
{ "$match": { _id:ObjectId("5dbdacc28cffef0b94580dbd") } },
{ "$addFields": {
"totalBoth": {
"$add": [
{ "$size": "$comments" },
{ "$size": {
"$reduce": {
"input": "$comments.replies",
"initialValue": [],
"in": {
"$concatArrays": [ "$$value", "$$this" ]
}
}
}}
]
}
}}
])
Noting that an "array of arrays" is the effect of an expression like $comments.replies, so hence the operation to make these into a single array where you can measure all elements.
Try using the $unwind to flatten the list you get from the $project before using $count.
This is another way of getting the result.
Input documents:
{ "_id" : 1, "array1" : [ { "array2" : [ { id: "This is a test!"}, { id: "test1" } ] }, { "array2" : [ { id: "This is 2222!"}, { id: "test 222" }, { id: "222222" } ] } ] }
{ "_id" : 2, "array1" : [ { "array2" : [ { id: "aaaa" }, { id: "bbbb" } ] } ] }
The query:
db.arrsizes2.aggregate( [
{ $facet: {
array1Sizes: [
{ $project: { array1Size: { $size: "$array1" } } }
],
array2Sizes: [
{ $unwind: "$array1" },
{ $project: { array2Size: { $size: "$array1.array2" } } },
],
} },
{ $project: { result: { $concatArrays: [ "$array1Sizes", "$array2Sizes" ] } } },
{ $unwind: "$result" },
{ $group: { _id: "$result._id", total1: { $sum: "$result.array1Size" }, total2: { $sum: "$result.array2Size" } } },
{ $addFields: { total: { $add: [ "$total1", "$total2" ] } } },
] )
The output:
{ "_id" : 2, "total1" : 1, "total2" : 2, "total" : 3 }
{ "_id" : 1, "total1" : 2, "total2" : 5, "total" : 7 }

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 aggregate multiple group by top fields and array fields

My collection will look like this,
{
"_id" : ObjectId("591c5971240033283736860a"),
"status" : "Done",
"createdDate" : ISODate("2017-05-17T14:09:20.653Z")
"communications" : [
{
"communicationUUID" : "df07948e-4a14-468e-beb1-db55ff72b215",
"communicationType" : "CALL",
"recipientId" : 12345,
"createdDate" : ISODate("2017-05-18T14:09:20.653Z")
"callResponse" : {
"Status" : "completed",
"id" : "dsd45554545ds92a9bd2c12e0e6436d",
}
}
]}
{
"_id" : ObjectId("45sdsd59124003345121450a"),
"status" : "ToDo",
"createdDate" : ISODate("2017-05-17T14:09:20.653Z")
"communications" : [
{
"communicationUUID" : "45sds55-4a14-468e-beb1-db55ff72b215",
"communicationType" : "CALL",
"recipientId" : 1234,
"createdDate" : ISODate("2017-05-18T14:09:20.653Z")
"callResponse" : {
"Status" : "completed",
"id" : "84fe862f1924455dsds5556436d",
}
}
]}
Currently I am writing two aggregate query to achieve my requirement and my query will be below
db.collection.aggregate(
{ $project: {
dayMonthYear: { $dateToString: { format: "%d/%m/%Y", date: "$createdDate" } },
status: 1,
}},
{ $group: {
_id: "$dayMonthYear",
Pending: { $sum: { $cond : [{ $eq : ["$status", "ToDo"]}, 1, 0]} },
InProgress: { $sum: { $cond : [{ $eq : ["$status", "InProgress"]}, 1, 0]} },
Done: { $sum: { $cond : [{ $eq : ["$status", "Done"]}, 1, 0]} },
Total: { $sum: 1 }
}}
My output will be,
{"_id" : "17/05/2017", "Pending" : 1.0, "InProgress" : 0.0, "Done" : 1.0, "Total" : 2.0 }
Using above query I can able to get count but I need to find the count based on communication Status too so I am writing one more query to achieve,
db.collection.aggregate(
{"$unwind":"$communications"},
{ $project: {
dayMonthYear: { $dateToString: { format: "%d/%m/%Y", date: "$createdDate" } },
communications: 1
}},
{ "$group": {
_id: "$dayMonthYear",
"total_call": { $sum: { $cond : [{ $or : [ { $eq: [ "$communications.callResponse.Status", "failed"] },
{ $eq: [ "$communications.callResponse.Status", "busy"] },
{ $eq: [ "$communications.callResponse.Status", "completed"] },
{ $eq: [ "$communications.callResponse.Status", "no-answer"] }
]}, 1, 0 ] }},
"engaged": { $addToSet: { $cond : [{ $eq : ["$communications.callResponse.Status", "completed"]},
"$communications.recipientId", "null" ]} },
"not_engaged": { $addToSet: { $cond: [{ $or : [ { $eq: [ "$communications.callResponse.Status", "failed"] },
{ $eq: [ "$communications.callResponse.Status", "busy"] },
{ $eq: [ "$communications.callResponse.Status", "no-answer"] } ]},
"$communications.recipientId", "null" ] }}
}},
{ "$project": {
"_id": 1,
"total_call": 1,
"engaged": { "$setDifference": [ "$ngaged", ["null"] ] },
"not_engaged": { "$setDifference": [ "$not_engaged", ["null"] ] },
}},
{ "$project": {
"total_call": 1,
"engaged": { "$size": "$engaged" },
"not_engaged": { "$size": { "$setDifference": [ "$not_engaged", "$engaged" ] }},
}})
My output will be,
{"_id" : "18/05/2017", "total_call" : 2.0, "engaged" : 2, "not_engaged" : 0}
Using above query I can able to get count but I want to achieve it in single query
I am looking for output like
{"_id":"17/05/2017", "Pending" : 1.0, "InProgress" : 0.0, "Done" : 1.0, "total_call" : 0, "engaged" : 0, "not_engaged" : 0}
{"_id":"18/05/2017", "Pending" : 0.0, "InProgress" : 0.0, "Done" : 0.0, "total_call" : 2, "engaged" : 2, "not_engaged" : 0}
Can anyone suggest or provide me good way to get above result.
You can use $concatArrays to merge the status& createdDate documents followed by $group to count the occurrences.
db.collection.aggregate([
{
"$project": {
"statusandcreateddate": {
"$concatArrays": [
[
{
"status": "$status",
"createdDate": "$createdDate"
}
],
{
"$map": {
"input": "$communications",
"as": "l",
"in": {
"status": "$$l.callResponse.Status",
"createdDate": "$$l.createdDate"
}
}
}
]
}
}
},
{
"$unwind": "$statusandcreateddate"
},
{
"$group": {
"_id": {
"$dateToString": {
"format": "%d/%m/%Y",
"date": "$statusandcreateddate.createdDate"
}
},
"total_call": {
"$sum": {
"$cond": [
{
"$or": [
{
"$eq": [
"$statusandcreateddate.status",
"failed"
]
},
{
"$eq": [
"$statusandcreateddate.status",
"busy"
]
},
{
"$eq": [
"$statusandcreateddate.status",
"completed"
]
},
{
"$eq": [
"$statusandcreateddate.status",
"no-answer"
]
}
]
},
1,
0
]
}
},
"engaged": {
"$sum": {
"$cond": [
{
"$eq": [
"$statusandcreateddate.status",
"completed"
]
},
1,
0
]
}
},
"not_engaged": {
"$sum": {
"$cond": [
{
"$or": [
{
"$eq": [
"$statusandcreateddate.status",
"failed"
]
},
{
"$eq": [
"$statusandcreateddate.status",
"busy"
]
},
{
"$eq": [
"$statusandcreateddate.status",
"no-answer"
]
}
]
},
1,
0
]
}
},
"Pending": {
"$sum": {
"$cond": [
{
"$eq": [
"$statusandcreateddate.status",
"ToDo"
]
},
1,
0
]
}
},
"InProgress": {
"$sum": {
"$cond": [
{
"$eq": [
"$statusandcreateddate.status",
"InProgress"
]
},
1,
0
]
}
},
"Done": {
"$sum": {
"$cond": [
{
"$eq": [
"$statusandcreateddate.status",
"Done"
]
},
1,
0
]
}
}
}
}
])