MongoDB aggregation: group and push multiple attributes - mongodb

I am having a problem with my query using MongoDB aggregation.
I have a collection as follows:
[
{
id: 1,
name: 'cash',
amount: 10,
},
{
id: 2,
name: 'IPT',
amount: 10,
terminal_type: 'inside',
card: {
id: 1,
name: 'visa',
},
},
{
id: 2,
name: 'IPT',
amount: 10,
terminal_type: 'outside',
card: {
id: 1,
name: 'visa',
},
},
]
Expected result:
[
{
id: 1,
name: 'cash',
amount: 10,
},
{
id: 2,
name: 'IPT',
amount: 20,
cards: [
{
id: 1,
name: 'visa',
amount: 20,
},
],
terminals: [
{
name: 'inside',
amount: 10,
},
{
name: 'outside',
amount: 10,
},
],
},
]
What I have tried:
{
$group: {
_id: {
id: '$id',
card_id: '$card.id',
terminal_type: '$terminal_type',
},
name: {$first: '$name'},
amount: {$sum: '$amount'},
card_name: {$sum: '$card.name'},
}
},
{
$group: {
_id: {
id: '$id',
card_id: '$_id.card_id',
},
name: {$first: '$name'},
amount: {$sum: '$amount'},
card_name: {$first: '$card_name'},
terminals: {
$push: {
{ $cond: [
{$ifnull: ['$terminal_type', false]},
{
type: '$terminal_type',
amount: '$amount',
},
'$$REMOVE',
]
}
}
}
}
},
{
$group: {
_id: '$_id.id',
name: {$first: '$name'},
amount: {$sum: '$amount'},
cards: {
$push: {
{ $cond: [
{$ifnull: ['$id.card_id', false]},
{
id: '$_id.card_id',
name: '$card_name',
amount: '$amount',
},
'$$REMOVE',
],
},
},
terminals: // This is the problem where I can't figure out how get this value
}
}
I considered to unwind terminals before the last group pipeline but I ended up getting duplicate documents which make the sum for the amount incorrect. Can anyone help me to solve this or point me to where I can read and understand more about this? Thank you very much.

One option to go is to $group and then $reduce:
db.collection.aggregate([
{$group: {
_id: "$id",
name: {$first: "$name"},
amount: {$sum: "$amount"},
terminals: {$push: {name: "$terminal_type", amount: "$amount"}},
cards: {$push: {id: "$card.id", name: "$card.name", amount: "$amount"}},
cardIds: {$addToSet: "$card.id"},
terminalNames: {$addToSet: "$terminal_type"}
}},
{ $project: {
_id: 0, id: "$_id", name: 1, amount: 1,
cards: {
$map: {
input: "$cardIds",
as: "card",
in: {
id: "$$card",
amount: {$reduce: {
input: "$cards",
initialValue: 0,
in: {$add: [
"$$value",
{$cond: [{$eq: ["$$card", "$$this.id"]},"$$this.amount", 0]}
]}
}},
name: {$reduce: {
input: "$cards",
initialValue: "",
in: {$cond: [{$eq: ["$$this.id", "$$card"]}, "$$this.name", "$$value"]}
}}
}
}},
terminals: {
$map: {
input: "$terminalNames",
as: "terminal",
in: {
name: "$$terminal",
amount: {$reduce: {
input: "$terminals",
initialValue: 0,
in: {$add: [
"$$value",
{$cond: [{$eq: ["$$terminal", "$$this.name"]}, "$$this.amount", 0]}
]}
}}
}
}
}
}}
])
See how it works on the playground example
Another option may be to use $unwind as you mentioned:
db.collection.aggregate([
{$group: {
_id: "$id",
name: {$first: "$name"},
amount: {$sum: "$amount"},
terminals: {$push: {
type: "terminal",
name: "$terminal_type",
key: "$terminal_type",
amount: "$amount"
}},
cards: {$push: {
type: "card",
id: "$card.id",
key: "$card.id",
name: "$card.name",
amount: "$amount"
}}
}},
{$project: {amount: 1, name: 1, docs: {$concatArrays: ["$cards", "$terminals"]}}},
{$unwind: "$docs"},
{$group: {
_id: {_id: "$_id", type: "$docs.type", key: "$docs.key"},
amount: {$sum: "$docs.amount"},
name: {$first: "$docs.name"},
dataAmount: {$first: "$amount"},
dataName: {$first: "$name"}
}},
{$group: {
_id: "$_id._id",
amount: {$first: "$dataAmount"},
name: {$first: "$dataName"},
terminals: {$push: {
$cond: [
{$and: [{$eq: ["$_id.type", "terminal"]}, {$gt: ["$name", null]}]},
{amount: "$amount", name: "$name"},
"$$REMOVE"
]
}},
cards: {$push: {
$cond: [
{$and: [{$eq: ["$_id.type", "card"]}, {$gt: ["$name", null]}]},
{amount: "$amount", name: "$name", id: "$_id.key"},
"$$REMOVE"
]
}}
}}
])
See how it works on the playground example

Related

Fill data with NULL value if it is not present in the timeperiod using mongodb aggregation pipeline

I have to write an aggreagtion pipeline in which I will pass:
Timestamps of start date and end data for a day
I have to divide the data into 30min buckets and find data in between that buckets like:
2023-01-16T00:30:00.000+00:00 , 2023-01-16T01:00:00.000+00:00, 2023-01-16T01:30:00.000+00:00 and so on.
If data is not present in any particular bucket fill the values of that bucketa with zero but give the timestamp like:
2023-01-16T01:00:00.000+00:00 ther is no data give {timestamp:2023-01-16T01:00:00.000+00:00,a:0,b:0,c:0}
I have done the following:
[{
$match: {
$and: [
{
timestamp: {
$gte: ISODate('2023-01-16T00:00:00.000Z'),
$lt: ISODate('2023-01-16T23:59:59.000Z')
}
}
]
}
}, {
$group: {
_id: {
$toDate: {
$subtract: [
{
$toLong: '$timestamp'
},
{
$mod: [
{
$toLong: '$timestamp'
},
1800000
]
}
]
}
},
in: {
$sum: '$a'
},
out: {
$sum: '$b'
},
Count: {
$sum: 1
}
}
}, {
$addFields: {
totalIn: {
$add: [
'$in',
'$out'
]
},{
$sort: {
_id: 1
}
}]
Result is:
[{
"_id": {
"2023-01-16T12:00:00.000+00:00"
}
},
"totalIn": 397,
"count":22
},
{
"_id": {
"2023-01-16T01:30:00.000+00:00"
}
},
"totalIn": 222,
"count":2
}
...]
expected result:
[{
"_id": {
"2023-01-16T12:00:00.000+00:00"
}
},
"totalIn": 397,
"count":22
},
{
"_id": {
"2023-01-16T12:30:00.000+00:00"
}
},
"totalIn": 0,
"count":0
},
{
"_id": {
"2023-01-16T01:00:00.000+00:00"
}
},
"totalIn": 0,
"count":0
},
{
"_id": {
"2023-01-16T12:00:00.000+00:00"
}
},
"totalIn": 222,
"count":2
}
...]
One option is to use $range with $dateAdd:
db.collection.aggregate([
{$match: {timestamp: {
$gte: startDate,
$lt: endDate
}}},
{$group: {
_id: {$dateTrunc: {date: "$timestamp", unit: "minute", binSize: 30}},
in: {$sum: "$a"},
out: {$sum: "$b"},
count: {$sum: 1}
}},
{$group: {
_id: 0,
data: {$push: {
timestamp: "$_id",
totalIn: {$add: ["$in", "$out"]},
count: "$count"
}}
}},
{$project: {
_id: 0, data: 1,
bins: {$map: {
input: {$range: [
0,
{$multiply: [
{$dateDiff: {
startDate: startDate,
endDate: endDate,
unit: "hour"
}},
2
]}
]},
in: {$dateAdd: {
startDate: startDate,
unit: "minute",
amount: {$multiply: ["$$this", 30]}
}}
}}
}},
{$unwind: "$bins"},
{$set: {data: {$filter: {
input: "$data",
cond: {$eq: ["$bins", "$$this.timestamp"]}
}}}},
{$project: {
_id: "$bins",
count: {$ifNull: [{$first: "$data.count"}, 0]},
totalIn: {$ifNull: [{$first: "$data.totalIn"}, 0]}
}}
])
See how it works on the playground example

Nodejs MongoDb add column to query that is a count of specific records

Considering the following document structure:
{_id: 1, name: 'joe', snapshot: null, age: 30}
{_id: 2, name: 'joe', snapshot: 'snapshot1', age: 30}
{_id: 3, name: 'joe', snapshot: 'snapshot15', age: 30}
{_id: 4, name: 'joe', snapshot: 'snapshot23', age: 30}
How would I perform a query that groups on the name field and adds an additional field that is a count of the remaining records containing subtree: 'additionalinfo'. It would look like this:
{_id: 1, name: 'joe', snapcount: 3, age: 30}
I've been able to group using aggregations but I can't quite get it like this.
My own solution:
I ultimately restructured my data to look like this instead:
{
_id: 1,
name: 'joe',
snapshots: [
{name: 'snap17', id: 1},
{name: 'snap15', id: 2},
{name: 'snap14', id: 3}
],
age: 30
}
This allows me to just check snapshots.length to solve my original problem. However; the answers in this post where very helpful and answered the original question.
Adding another aggregation query to do it: playground link: try it
db.collection.aggregate([
{
$match: {
"snapshot": {
$exists: true,
$ne: null
}
}
},
{
$group: {
_id: "$name",
snapcount: {
$sum: 1
},
age: {
"$first": "$age"
},
name: {
"$first": "$name"
}
}
},
{
"$unset": "_id"
}
])
Based on the comments, the query worked for OP:
db.collection.aggregate([
{
$match: {
"snapshot": {
$exists: true,
$ne: null
}
}
},
{
$group: {
_id: "$name",
snapcount: {
$sum: 1
},
age: {
"$first": "$age"
},
name: {
"$first": "$name"
},
id: {
"$first": "$_id"
}
}
},
{
"$unset": "_id"
}
])
Here's one way you could do it.
db.collection.aggregate([
{
"$group": {
"_id": "$name",
"name": {"$first": "$name"},
"age": {"$first": "$age"},
"snapcount": {
"$sum": {
"$cond": [
{"$eq": [{"$type": "$snapshot"}, "string"]},
1,
0
]
}
}
}
},
{"$unset": "_id"}
])
Try it on mongoplayground.net.

MongoDB Aggregation with Sum and Multiple Group Results

Let's say I have these collections members and positions
[
{
"church":"60dbb265a75a610d90b45c6b", "parentId":"60dbb265a75a610d90b45c6b", name: "Jonah John", status: 1, birth: "1983-01-01", position: "60f56f59-08be-49ec-814a-2a421f21bc08"
},
{
"church":"60dbb265a75a610d90b45c6b", "parentId":"60dbb265a75a610d90b45c6b", name: "March John", status: 1, birth: "1981-01-23", position: "60f56f59-08be-49ec-814a-2a421f21bc08"
},
{
"church":"60dbb265a75a610d90b45c6b", "parentId":"60dbb265a75a610d90b45c6b",name: "Jessy John", status: 0, birth: "1984-08-01", position: "e5bba609-082c-435a-94e3-0997fd229851"
}
]
[
{_id: "60f56f59-08be-49ec-814a-2a421f21bc08", name: "Receptionist"},
{_id: "5c78ba5a-3e6c-4d74-8d4a-fa23d02b8003", name: "Curtain"},
{_id: "e5bba609-082c-435a-94e3-0997fd229851", name: "Doorman"}
]
I want to aggregate in a way I can get:
inactiveMembers
activeMembers
totalMembers
totalPositionsOcuppied
And two arrays with:
positionsOcuppied {name, quantity}
birthdays {month, quantity.
I need an output like this:
{
"_id": {
"church":"60dbb265a75a610d90b45c6b",
"parentId":"60dbb265a75a610d90b45c6b"
},
"inactiveMembers":1,
"activeMembers":2,
"totalMembers":3,
"birthdays": [
{january:2}, {august:1}
],
"positionsOcuppied": [
{Doorman: 1}, {Receptionist:2}
],
"totalPositionsOcuppied": 3
}
How can I do that?
PS.: Very sorry for unclear values...
Update:
$addFields with birthMonth string
$lookup to add positions
$facet to $group by birthdays, positionsOcuppied, and all docs tougher as other
$map to format birthdays and positionsOcuppied
Format the answer
db.people.aggregate([
{$addFields: {
birthMonth: {
$arrayElemAt: [
["","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],
{$month: {$toDate: "$birth"}}
]
}
}
},
{$lookup: {from: "positions", localField: "position", foreignField: "_id",
as: "position"}},
{$facet: {
birthdays: [{$group: {_id: "$birthMonth", count: {$sum: 1}}}],
positionsOcuppied: [{$group: {_id: {$first: "$position.name"}, count: {$sum: 1}}}],
other: [
{$group: {_id: 0,
activeMembers: {$sum: "$status"},
totalMembers: {$sum: 1},
church: {$first: "$church"},
parentId: {$first: "$parentId"},
totalPositionsOcuppied: {$sum: {$size: "$position"}}
}
}
]
}
},
{$set: {
birthdays: {
$map: {input: "$birthdays", in: [{k: "$$this._id", v: "$$this.count"}]}
},
positionsOcuppied: {
$map: {input: "$positionsOcuppied", in: [{k: "$$this._id", v: "$$this.count"}]}
},
other: {$first: "$other"}
}
},
{$set: {
"other.birthdays": {
$map: {input: "$birthdays", in: {$arrayToObject: "$$this"}}
},
"other.positionsOcuppied": {
$map: {input: "$positionsOcuppied", in: {$arrayToObject: "$$this"}}
},
"other.inactiveMembers": {
$subtract: ["$other.totalMembers", "$other.activeMembers"]
},
"other._id": {church: "$other.church", parentId: "$other.parentId"},
birthdays: "$$REMOVE",
"other.church": "$$REMOVE",
"other.parentId": "$$REMOVE",
positionsOcuppied: "$$REMOVE"
}
},
{$replaceRoot: {newRoot: "$other"}}
])
See how it works on the playground example

mongo nested aggregation with join

I've following tenant collection:
{id: 1, name: "T1", type: "DEFAULT", state: "ACTIVE"},
{id: 2, name: "T2", type: "DEFAULT", state: "DISABLED"},
{id: 3, name: "T3", type: "STANDARD", state: "ACTIVE"},
{id: 4, name: "T4", type: "TRIAL", state: "DELETED"},
{id: 5, name: "T5", type: "DEFAULT", state: "DISABLED"}
and then second collection with options:
{id:1, tenantId: 1, opt: "OPERATING"},
{id:2, tenantId: 2, opt: "OPERATING"},
{id:3, tenantId: 3, opt: "POSTPONED"},
{id:4, tenantId: 4, opt: "DELETED"},
{id:5, tenantId: 5, opt: "POSTPONED"}
Id' like to aggregate this collections to get umber of tenant types grouped with number of operations, but I'd like to remove all DELETED tenants and all DELETED options from search. Something like this:
{type: "DEFAULT", count: 3, opts: {operating: 2, postponed: 1}}
{type: "STANDARD", count: 1, opts: {postponed: 1}}
Grouping the tenants is fine, but I don't know what should I use for that next grouping of options.
db.tenant.aggregate([
{$match: { state: {$ne: "DELETED"}}},
{$lookup: {
from: "option",
localField: "_id",
foreignField: "tenantId",
as: "options"
}},
{$group {
_id: "$type",
count: {$sum: 1}
}}
])
$group by type and get group of ids
$lookup with pipeline match $in condition for tenantId
$group by opt and get count of option
$project to show fields in k and v format
$project to show required fields, $size to count total tenant and $arrayToObject convert opts array to object
db.tenant.aggregate([
{ $match: { state: { $ne: "DELETED" } } },
{
$group: {
_id: "$type",
ids: { $push: "$id" }
}
},
{
$lookup: {
from: "options",
let: { ids: "$ids" },
pipeline: [
{ $match: { opt: { $ne: "DELETED" }, $expr: { $in: ["$tenantId", "$$ids"] } } },
{
$group: {
_id: "$opt",
count: { $sum: 1 }
}
},
{
$project: {
_id: 0,
k: "$_id",
v: "$count"
}
}
],
as: "opts"
}
},
{
$project: {
_id: 0,
type: "$_id",
count: { $size: "$ids" },
opts: { $arrayToObject: "$opts" }
}
}
])
Playground

Count image by date for each userId in mongoDB and map response with user emailId

I have two collection named User and ImageDetail.
I need to find image count by date for each user, and response list should contain user email Id with userId.
Collection Data:
ImageDetail collection
[
{
useId: 'A1',
images: [
{date: '01/02/2018', image: 'image1'},
{date: '01/02/2018', image: 'image2'},
{date: '02/02/2018', image: 'image3'},
]
},
{
useId: "A2",
images: [
{date: '01/02/2018', image: 'image4'},
{date: '02/02/2018', image: 'image5'},
{date: '02/02/2018', image: 'image6'},
{date: '03/02/2018', image: 'image7'},
]
}
]
User collection:
[
{
userId: 'A1',
emailId: 'user1#gmail.com',
},
{
userId: 'A2',
emailId: 'user2#gmail.com',
}
]
Looking for output:
[
{
userId: 'A1',
emailId: 'user1#gmail.com',
images: [
{date: '01/02/2018', count: '2'}.
{date: '02/02/2018', count: '1'}.
],
},
{
userId: 'A2',
emailId: 'user2#gmail.com',
images: [
{date: '01/02/2018', count: '1'}.
{date: '02/02/2018', count: '2'}.
{date: '03/02/2018', count: '1'}.
],
}
]
Use this aggregation:
db.getCollection("ImageDetail").aggregate([
{
$unwind: "$images"
},
{
$group: {
_id: {date: "$images.date", userId: "$userId"},
count: {$sum: 1},
}
},
{
$group: {
_id: "$_id.userId",
images: {$push: {date: "$_id.date", count: "$count"}}
}
},
{
$lookup: {
from: "user",
localField: "_id",
foreignField: "userId",
as: "$match"
}
},
{
$unwind: "$match"
},
{
$project: {
images: "$images",
userId: "$_id",
email: "$match.emailId"
}
}
])