mongo nested aggregation with join - mongodb

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

Related

mongodb - Merge object arrays based on key

In a mongodb database, I have the following data:
// db.people
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father" }],
// A bunch of other data I don't care about
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
// A bunch of other data I don't care about
},
]
I would like to query the people, and select only the fields I've shown, but have extra data in relatedPeople (id + relation + name)
So the desired output would be:
[
{
_id: ObjectId("..."),
id: 111111111,
name: "George",
relatedPeople: [{ id: 222222222, relation: "child", name: "Jacob" }],
},
{
_id: ObjectId("..."),
id: 222222222,
name: "Jacob",
relatedPeople: [{ id: 111111111, relation: "father", name: "George" }],
},
{
_id: ObjectId("..."),
id: 333333333,
name: "some guy",
relatedPeople: [],
},
]
I can get something close, with this query:
db.people.aggregate([
// { $match: { /** ... */ }, },
{
$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{
$project: {
id: 1,
name: 1,
relatedPeople: 1,
relatedPeople2: 1,
}
}
]);
But the data is split between two fields. I want to merge each object in the arrays by their id, and place the result array in relatedPeople
I found this question, but that merge is done over a range and uses $arrayElementAt which I can't use
I also tried looking at this question, but I couldn't get the answer to work (Kept getting empty results)
You can add one step using $arrayElementAt with $indexOfArray:
db.people.aggregate([
// { $match: { /** ... */ }, },
{$project: {id: 1, name: 1, relatedPeople: 1}},
{$lookup: {
from: "people",
let: { relatedPeopleIds: "$relatedPeople.id" },
pipeline: [
{ $match: { $expr: { $in: ["$id", "$$relatedPeopleIds"] } } },
{
$project: {
id: 1,
name: 1,
},
},
],
as: "relatedPeople2",
},
},
{$set: {
relatedPeople: {$map: {
input: "$relatedPeople",
in: {$mergeObjects: [
"$$this",
{$arrayElemAt: [
"$relatedPeople2",
{$indexOfArray: ["$relatedPeople2.id", "$$this.id"]}
]}
]}
}}
}},
{$unset: "relatedPeople2"}
])
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 aggregate to return result only if the lookup field has length

I have two collections users and profiles. I am implementing a search with the following query:
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
],
(error, result) => {
console.log("RESULT ", result);
}
);
What this does is that its searches for the city in the profiles collection and when there is not search match then profileId becomes an empty array. What I really want is that if the profileId is an empty array then I don't want to return the other fields in the documents too. It should empty the array. Below is my current returned result.
RESULT [
{
_id: 60cabe38e26d8b3e50a9db21,
isDogSitter: true,
firstName: 'Test',
lastName: 'Sitter',
email: 'test#user.com',
profileId: []
}
]
Add $match pipeline stage after the $lookup pipeline stage and
add the empty array condition check over there.
User.aggregate(
[
{
$match: {
_id: { $ne: req.user.id },
isDogSitter: { $eq: true },
profileId: { $exists: true }
}},
{
$project: {
firstName: 1,
lastName: 1,
email: 1,
isDogSitter: 1,
profileId: 1,
}},
{
$lookup: {
from: "profiles",
pipeline: [
{
$project: {
__v: 0,
availableDays: 0,
}},
{
$match: {
city: search
}}
],
as: "profileId",
}}
{
$match: { // <-- Newly added $match condition
"profileId": {"$ne": []}
},
},
],
(error, result) => {
console.log("RESULT ", result);
}
);

$project in $lookup mongodb

I have a query, that use $lookup to "join" two models, after this i use $project to select olny the fields that i need, but my $project brings an arrray of objects (user_detail) that contains more data that i need. I want only two fields (scheduleStart and scheduleEnd) of my result.
My query:
User.aggregate([{
$match: {
storeKey: req.body.store,
}
},
{
$group: {
_id: {
id: "$_id",
name: "$name",
cpf: "$cpf",
phone: "$phone",
email: "$email",
birthday: "$birthday",
lastName: "$lastname"
},
totalServices: {
$sum: "$services"
},
}
},
{
$lookup: {
from: "schedules",
localField: "_id.phone",
foreignField: "customer.phone",
as: "user_detail"
}
},
{
$project: {
_id: 1,
name: 1,
name: 1,
cpf: 1,
phone: 1,
email: 1,
birthday: 1,
totalServices: 1,
totalValue: { $sum : "$user_detail.value" },
count: {
$sum: 1
},
user_detail: 1
}
},
Result of query:
count: 1
totalServices: 0
totalValue: 73
user_detail: Array(2)
0:
...
paymentMethod: 0
paymentValue: "0"
scheduleDate: "2018-10-02"
scheduleEnd: "2018-10-02 08:40"
scheduleStart: "2018-10-02 08:20"
status: 3
store: "5b16cceb56a44e2f6cd0324b"
updated: "2018-11-27T13:30:21.116Z"
1:
...
paymentMethod: 0
paymentValue: "0"
scheduleDate: "2018-11-27"
scheduleEnd: "2018-11-27 00:13"
scheduleStart: "2018-11-27 00:03"
status: 2
store: "5b16cceb56a44e2f6cd0324b"
updated: "2018-11-27T19:33:39.498Z"
_id:
birthday: "1992-03-06"
email: "csantosgrossi#gmail.com"
id: "5bfed8bd70de7a383855f09e"
name: "Chris Santos G"
phone: "11969109995"
...
Result that i need:
count: 1
totalServices: 0
totalValue: 73
user_detail: Array(2)
0:
scheduleEnd: "2018-10-02 08:40"
scheduleStart: "2018-10-02 08:20"
1:
scheduleEnd: "2018-11-27 00:13"
scheduleStart: "2018-11-27 00:03"
_id:
birthday: "1992-03-06"
email: "csantosgrossi#gmail.com"
id: "5bfed8bd70de7a383855f09e"
name: "Chris Santos G"
phone: "11969109995"
...
How can i do this with my query?
You can use $lookup 3.6 syntax to $project the fields inside the $lookup pipeline
User.aggregate([
{ "$lookup": {
"from": "schedules",
"let": { "id": "$_id.phone" },
"pipeline": [
{ "$match": { "$expr": { "$eq": ["$customer.phone", "$$id"] }}},
{ "$project": { "scheduleStart": 1, "scheduleEnd": 1 }}
],
"as": "user_detail"
}}
])
For version of mongo version > 3.6 this query should work for you:
User.aggregate([{
$match: {
storeKey: req.body.store,
}
},
{
$group: {
_id: {
id: "$_id",
name: "$name",
cpf: "$cpf",
phone: "$phone",
email: "$email",
birthday: "$birthday",
lastName: "$lastname"
},
totalServices: {
$sum: "$services"
},
}
},
{
$lookup: {
from: "schedules",
localField: "_id.phone",
foreignField: "customer.phone",
as: "user_detail"
}
},
{
$project: {
_id: 1,
name: 1,
name: 1,
cpf: 1,
phone: 1,
email: 1,
birthday: 1,
totalServices: 1,
totalValue: { $sum : "$user_detail.value" },
count: {
$sum: 1
},
user_detail: {
scheduleEnd: 1,
scheduleStart: 1,
}
}
},

MongoDB : create a view which is the union of several collections

I currently have a collection that i need to split in several smaller collections. Is there a way to make a View containing the union of all my smaller collections ?
According to the MongoDB Manual, i could use the $lookup operator in the pipeline, but it ends up being more like a "join" than an "union".
Here is an example of what i want to do :
Current collection :
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
Splitting into :
Collection_US
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
Collection_DE
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
And then, make a view :
View
{ _id: 1, name: "abc", country: "us" }
{ _id: 2, name: "def", country: "us" }
{ _id: 3, name: "123", country: "de" }
{ _id: 4, name: "456", country: "de" }
Is it possible to do this ?
This is the same modified of taminov's code.
db.createView('union_view', 'us', [
{
$facet: {
us: [
{$match: {}}
],
de: [
{$limit: 1},
{
$lookup: {
from: 'de',
localField: '__unexistingfield',
foreignField: '__unexistingfield',
as: '__col2'
}
},
{$unwind: '$__col2'},
{$replaceRoot: {newRoot: '$__col2'}}
]
},
},
{$project: {data: {$concatArrays: ['$us', '$de']}}},
{$unwind: '$data'},
{$replaceRoot: {newRoot: '$data'}}
])
its very hacky but will work for small collections. you may end up having to use a real collection if the collections are big.
db.createView('union_view', 'col1', [
{
$facet: {
col1: [
{ $match:{}}
],
col2: [
{ $limit:1},
{ $lookup:{
from: 'col2',
localField: '__unexistingfield',
foreignField: '__unexistingfield',
as: '__col2'
}},
{ $unwind:'$__col2'},
{ $replaceRoot: {newRoot: '$__col2'}}
]
},
},
{ $project: { filesFolders: {$setUnion: ['$files', '$folders']}}},
{ $unwind: '$filesFolders' },
{ $replaceRoot: {newRoot: '$filesFolders'}}
])