mongo db aggregate keep fields after group - mongodb

I have this aggregate:
[
{
$match: {_id: new ObjectId('xxx')}
},
{
$unwind: "$users"
},
{
$group: {
_id: '$users.status',
count: {$sum: 1}
}
},
{
$project: {
name: 1,
field1: 1,
field2: 1,
count: '$count'
}
}
]
The schema:
{
name: String,
field1: String,
field2: Schema.Types.Mixed,
users: [{
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
status: String
}]
}
The expected result:
{
name: 'NY',
field1: 'example',
field2: 'example2',
statuses: [
{
_id: 'ONLINE',
count: 20
},
{
_id: 'OFFLINE',
count: 120
},
{
_id: 'OTHER',
count: 230
}
]
}
This way I get group result just for the users statuses, and not for the other fields of the original object.

Related

MongoDB aggregate. Create new groups for non-existing items

My collection of documents contains information about users, their sessions and CRUD operations they performed during these sessions:
{
user_id: '1',
sessions: [
{
actions: [
{
type: 'create',
created_at: ISODate('2020-01-01T00:00:00'),
},
{
type: 'read',
created_at: ISODate('2022-01-01T00:00:00'),
},
{
type: 'read',
created_at: ISODate('2021-01-01T00:00:00'),
}
],
}
]
}
I need to get a summary for each user, which includes the amount of CRUD operations and the date of the last one:
{
user_id: '1',
actions: [
{
type: 'create',
last: ISODate('2020-01-01T00:00:00'),
count: 1,
},
{
type: 'read',
last: ISODate('2022-01-01T00:00:00'),
count: 2,
},
// Problematic part:
{
type: 'update',
last: null,
count: 0,
},
{
type: 'delete',
last: null,
count: 0,
},
]
}
I came up with this solution:
db.users.aggregate([
{$unwind:'$sessions'},
{$unwind:'$sessions.actions'},
{
$group:{
_id:{user_id:'$user_id', type:'$sessions.actions.type'},
last:{$max:'$sessions.actions.created_at'},
count:{$sum:1},
}
},
{
$group:{
_id:{user_id:'$_id.user_id'},
actions:{$push:{type:'$_id.type', last:'$last', count:'$count'}}
}
},
{
$project:{
_id:0,
user_id: '$_id.user_id',
actions: '$actions'
}
}
])
The problem here is that I cannot figure out, how can I add missing actions, like in 'update' and 'delete' in the example above
Try this,
db.collection.aggregate([
{
$unwind: "$sessions"
},
{
$unwind: "$sessions.actions"
},
{
$group: {
_id: {
user_id: "$user_id",
type: "$sessions.actions.type"
},
last: {
$max: "$sessions.actions.created_at"
},
count: {
$sum: 1
},
}
},
{
$group: {
_id: {
user_id: "$_id.user_id"
},
actions: {
$push: {
type: "$_id.type",
last: "$last",
count: "$count"
}
}
}
},
{
$project: {
_id: 0,
user_id: "$_id.user_id",
actions: {
"$function": {
"body": "function(doc) { const ops = {read:0, delete:0, update: 0, create: 0}; const actions = doc.actions; actions.forEach(action => { ops[action.type] = 1 }); Object.keys(ops).filter(key => ops[key] === 0).forEach(key => actions.push({count: 0, last: null, type: key})); return actions }",
"args": [
"$$ROOT"
],
"lang": "js"
}
},
}
},
])
Here, we use $function and provide a small JS function to populate the missing entries.
Playground link.

Get extra field in model summing all records from lookup document

Having this model:
const matchSchema = mongoose.Schema({
location: {type: mongoose.Types.ObjectId, ref: 'Location'},
datetime: Date,
teamAName: String,
teamBName: String,
teamA: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
teamB: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
teamAScore: {type: Number, default: 0},
teamBScore: {type: Number, default: 0},
pichichi: [{type: mongoose.Types.ObjectId, ref: 'Player'}],
mvp: {type: mongoose.Types.ObjectId, ref: 'Player', default:null},
});
"teamA" and "teamB" are lists containing the "_id" of every player. When retrieving a player, I want to retrieve the number of matches that he/she have played. How can I do that? Below my query just retrieving fields from model "Player"
class PlayerController {
getAll(req, res) {
Player.find()
.sort('firstname')
.exec(function(err, players) {
res.send(players);
});
}
}
So, instead of just having this list:
[
{
_id: new ObjectId("6232395d08663294b412d6a1"),
firstname: 'Sam',
lastname: 'Credington',
__v: 0
},
{
_id: new ObjectId("622479f39be8118a52af70e5"),
firstname: 'Santi',
lastname: 'Futsal',
__v: 0
},
{
_id: new ObjectId("6232399608663294b412d6b9"),
firstname: 'Tom',
lastname: 'Hendry',
__v: 0
}
]
I would like to have the amount of matches that every player played:
[
{
_id: new ObjectId("6232395d08663294b412d6a1"),
firstname: 'Sam',
lastname: 'Credington',
matches:6,
__v: 0
},
{
_id: new ObjectId("622479f39be8118a52af70e5"),
firstname: 'Santi',
lastname: 'Futsal',
matches:8,
__v: 0
},
{
_id: new ObjectId("6232399608663294b412d6b9"),
firstname: 'Tom',
lastname: 'Hendry',
matches: 2,
__v: 0
}
]
Here's one way you could do it.
db.players.aggregate([
{
"$lookup": {
"from": "matches",
"let": { "myId": "$_id" },
"pipeline": [
{
"$match": {
"$expr": {
"$in": [ "$$myId", { "$setUnion": [ "$teamA", "$teamB" ] } ]
}
}
},
{ "$count": "numMatches" }
],
"as": "matchCount"
}
},
{
"$set": {
"matches": {
"$ifNull": [ { "$first": "$matchCount.numMatches" }, 0 ]
}
}
},
{ "$unset": "matchCount" }
])
Try it on mongoplayground.net.

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

Aggregate string values to array of unique string values per field with mongo or mongoose and node.js

I need to aggregate "lastNames" and "occupations" for a "name" to get a result:
{
name: 'John',
occupations: ['software engineer', 'qa']
lastNames: ['Smith', 'Red', 'Doe']
}
input
name: 'John'
documents present in mongo:
{name: 'John', lastName: 'Smith', occupation: 'software engineer'}
{name: 'Steve', lastName: 'Smith', occupation: 'senior software engineer'}
{name: 'John', lastName: 'Doe', occupation: 'qa'}
{name: 'Steve', lastName: 'Doe', occupation: 'manager'}
{name: 'John', lastName: 'Red', occupation: 'software engineer'}
I started with this aggregation query:
Employees.aggregate([
{ $match: { name: name } },
{
$unwind: {
path: '$lastName',
},
},
{
$unwind: {
path: '$occupation',
},
},
{ $group: { _id: '$name' } },
]);
but this returns an empty array, so I kinda stuck as I never did aggregations before.
Is there a way to produce this required result?
Would be this one:
db.collection.aggregate([
{ $match: { name: "John" } },
{
$group: {
_id: "$name",
occupations: { $addToSet: "$occupation" },
lastNames: { $addToSet: "$lastName" },
}
},
{
$project: {
_id: 0,
name: "$_id",
occupations: 1,
lastNames: 2
}
}
])
Mongo playground

MongoDB - Help needed to make some aggregation

I am having a bad time trying to do an aggregation in MongoDB.
I need to cross some infos from each user and as a final result I want a list of users (where there is only one object for each user) and for each object there is some lists with distinct information.
1 - The createdAtList array must be ordered from the oldest to the newest date. The sumOfTotal means the current position total summed up with the previous sumOfTotal (Exemplified in the code below), not just the sum of the total's
2 - The categotyList must be ordered like: category1, category2, category3 ...
3 - The desired final result must be ordered like: user1, user2, user3 ...
Basically I need some help to do the following:
//List of docs from my collection:
[
{
_id: "doc1",
user: "user1",
category: "category1",
createdAt: "2018-01-01T00:00:00.000Z"
},
{
_id: "doc2",
user: "user1",
category: "category2",
createdAt: "2017-12-12T00:00:00.000Z",
},
{
_id: "doc3",
user: "user1",
category: "category1",
createdAt: "2017-12-12T00:00:00.000Z",
},
{
_id: "doc4",
user: "user1",
category: "category2",
createdAt: "2018-01-01T00:00:00.000Z"
},
{
_id: "doc5",
user: "user1",
category: "category3",
createdAt: "2017-11-11T00:00:00.000Z"
}
]
//Desired result:
{
user: "user1",
createdAtList: [ //list ordered by createdAt
{
createdAt: "2017-11-11T00:00:00.000Z",
total: 1,
sumOfTotal: 0
}
{
createdAt: "2017-12-12T00:00:00.000Z",
total: 2,
sumOfTotal: 3 //summed up with the previous
}
{
createdAt: "2018-01-01T00:00:00.000Z",
total: 2,
sumOfTotal: 5 //summed up with the previous
}
],
categotyList: [ //list ordered by category
{
category: "category1",
total: 2
},
{
category: "category2",
total: 2
},
{
category: "category3",
total: 1
}
]
},
...
Is possible to do this in the same aggregate?
I do not think it really makes sense to have the createdAtList.sumOfTotal field. I do not think the fields in an array should be dependent upon a particular order of the elements. If you want some field to contain the sum of the createdAtList.total field, I think there should only be one field (outside of the array). That being said, here is the query I came up with to give you the desired results (using "users" as the name of the collection):
db.users.aggregate([
{
$group: {
_id: {
user: "$user",
createdAt: "$createdAt"
},
total: { $sum: 1 },
category: { $push: "$category" }
}
},
{
$project: {
_id: 0,
user: "$_id.user",
createdAt: "$_id.createdAt",
total: "$total",
category: 1
}
},
{ $unwind: "$category" },
{
$group: {
_id: {
user: "$user",
category: "$category"
},
catTotal: { $sum: 1 },
createdAtList: {
$push: {
createdAt: "$createdAt",
total: "$total"
}
}
}
},
{
$project: {
_id: 0,
user: "$_id.user",
createdAtList: 1,
category: "$_id.category",
catTotal: 1
}
},
{ $unwind: "$createdAtList" },
{
$group: {
_id: "$user",
createdAtList: {
$addToSet: "$createdAtList"
},
categoryList: {
$addToSet: {
category: "$category",
total: "$catTotal"
}
}
}
},
{ $unwind: "$createdAtList" },
{ $sort: { "createdAtList.createdAt": 1 } },
{
$group: {
_id: "$_id",
createdAtList: {
$push: "$createdAtList"
},
categoryList: {
$first: "$categoryList"
}
}
},
{ $unwind: "$categoryList" },
{ $sort: { "categoryList.category": 1 } },
{
$group: {
_id: "$_id",
createdAtList: {
$first: "$createdAtList"
},
categoryList: {
$push: "$categoryList"
}
}
},
{
$project: {
_id: 0,
user: "$_id",
createdAtList: 1,
sumOfTotal: { $sum: "$createdAtList.total" },
categoryList: 1
}
},
{ $sort: { user: 1 } },
]).pretty()