Mongo $projection, can you flat a sub-document array? - arrays

I was wondering if there is a way to "flatten" though projection a nested sub-document array so I could use it to sum its entries based on type.
My document looks like this:
{
"order_id":12345,
"date":8/17/2019,
"payment":{
status:1,
transactions:[
{type: 1, amount:200},
{type: 2, amount:250},
{type: 3, amount:50},
{type: 4, amount:50},
]
}
}
I would like to see if you can "flatten" it to something like this using $project:
{
"order_id":12345,
"date":8/17/2019,
"status":1,
"type": 1,
"amount":200
},
{
"order_id":12345,
"date":8/17/2019,
"status":1,
"type": 2,
"amount":250
},
{
"order_id":12345,
"date":8/17/2019,
"status":1,
"type": 4,
"amount":50
},
{
"order_id":12345,
"date":8/17/2019,
"status":1,
"type": 4,
"amount":50
}
}
Primarily my goal is to aggregate all the amounts for transactions of type 1 & 3 and all the transactions with type 2 & 4.
Any help would be great.

The following query can get you the expected output:
db.check.aggregate([
{
$unwind:"$payment.transactions"
},
{
$project:{
"_id":0,
"order_id":1,
"date":1,
"status":"$payment.status",
"type":"$payment.transactions.type",
"amount":"$payment.transactions.amount"
}
}
]).pretty()
Output:
{
"order_id" : 12345,
"date" : "8/17/2019",
"status" : 1,
"type" : 1,
"amount" : 200
}
{
"order_id" : 12345,
"date" : "8/17/2019",
"status" : 1,
"type" : 2,
"amount" : 250
}
{
"order_id" : 12345,
"date" : "8/17/2019",
"status" : 1,
"type" : 3,
"amount" : 50
}
{
"order_id" : 12345,
"date" : "8/17/2019",
"status" : 1,
"type" : 4,
"amount" : 50
}

Related

get the sum after the $unwind and $lookup returns 0

Player collection:
{ "_id" : 1, "Name" : "John Aims", "Gender" : "M", "DoB" : ISODate("1990-01-01T00:00:00Z"), "Nationality" : "USA", "Hand" : "R", "YearTurnedPro" : 2010, "Tournament" : [ { "tournamentID" : 1, "TournamentYear" : 2016 }, { "tournamentID" : 2, "TournamentYear" : 2019 }, { "tournamentID" : 3, "TournamentYear" : 2021 } ] }
{ "_id" : 2, "Name" : "George Brown", "Gender" : "M", "DoB" : ISODate("1997-03-04T00:00:00Z"), "Nationality" : "GB", "Hand" : "L", "YearTurnedPro" : 2013, "Tournament" : [ { "tournamentID" : 2, "TournamentYear" : 2016 }, { "tournamentID" : 5, "TournamentYear" : 2019 } ] }
Tournament collection:
{ "_id" : ObjectId("626c18a3d880647a888888ff"), "TournamentID" : 1, "TournamentCode" : "GS1", "Position" : 8, "PrizeMoney" : 125000, "RankingPoints" : 250 }
{ "_id" : ObjectId("626c18c2d880647a888888ff"), "TournamentID" : 2, "TournamentCode" : "GS1", "Position" : 4, "PrizeMoney" : 250000, "RankingPoints" : 500 }
{ "_id" : ObjectId("626c18ddd880647a888888ff"), "TournamentID" : 3, "TournamentCode" : "GS1", "Position" : 1, "PrizeMoney" : 1000000, "RankingPoints" : 2000 }
1st Question:
Hello, I want to get the sum of ranking points of each player.
I have tried:
db.Player.aggregate([
{"$unwind" : "$Tournament"},
{"$lookup":
{"from":"Tournament",
"localField":"Tournament.tournamentID",
"foreignField":"TournamentID",
"as":"Tennis-player"}},
{ "$group": {
"_id": { Name:"$Name" },
"total_qty": { "$sum": "$Tennis-player.PrizeMoney" }
}}
])
But I get for every played the sum is 0.
I can show it on playground as it is using more than 1 collection.
2nd question:
Would it be better to create only 1 collections with all the data?
$unwind
$lookup
$set - As from stage 2 Tennis-player returns an array with guarantee only 1 document in array. Use $first to get the first document in Tennis-player array field to become a document field.
$group
db.Player.aggregate([
{
"$unwind": "$Tournament"
},
{
"$lookup": {
"from": "Tournament",
"localField": "Tournament.tournamentID",
"foreignField": "TournamentID",
"as": "Tennis-player"
}
},
{
$set: {
"Tennis-player": {
"$first": "$Tennis-player"
}
}
},
{
"$group": {
"_id": {
Name: "$Name"
},
"total_qty": {
"$sum": "$Tennis-player.PrizeMoney"
}
}
}
])
Sample Mongo Playground
Alternative:
$lookup - Work $lookup with an Array
$project - Decorate output documents. Create total_qty field and use $reduce to perform sum operation of Tennic-player.PrizeMoney.
db.Player.aggregate([
{
"$lookup": {
"from": "Tournament",
"localField": "Tournament.tournamentID",
"foreignField": "TournamentID",
"as": "Tennis-player"
}
},
{
"$project": {
"_id": {
Name: "$Name"
},
"total_qty": {
"$reduce": {
"input": "$Tennis-player",
"initialValue": 0,
"in": {
$sum: [
"$$value",
"$$this.PrizeMoney"
]
}
}
}
}
}
])
Sample Mongo Playground (Alternative)

How to match foreign keys in MongoDB and perform following complex queries?

My database has 3 Collections :
Tour, 2) turism_ind, 3) customer
> db.tour.find({}).pretty()
{
"_id" : ObjectId("622385ab1b68d9136e48ba51"),
"source" : "Pune",
"destination" : "Kashmir"
}
{
"_id" : ObjectId("622385ba1b68d9136e48ba52"),
"source" : "Mumbai",
"destination" : "Shilong"
}
{
"_id" : ObjectId("622385ce1b68d9136e48ba53"),
"source" : "Nashik",
"destination" : "Goa"
}
> db.turism_ind.find({}).pretty()
{
"_id" : ObjectId("6223885d1b68d9136e48ba57"),
"ind_name" : "Veena World",
"package" : [
{
"pkg_id" : 111,
"tour_id" : ObjectId("622385ba1b68d9136e48ba52"),
"cost" : 85000
}
],
"cust_review" : [
{
"cust_id" : ObjectId("622387fa1b68d9136e48ba56"),
"rating" : 4
}
]
}
{
"_id" : ObjectId("622389191b68d9136e48ba58"),
"ind_name" : "GK Travels",
"package" : [
{
"pkg_id" : 222,
"tour_id" : ObjectId("622385ba1b68d9136e48ba52"),
"cost" : 82000
},
{
"pkg_id" : 223,
"tour_id" : ObjectId("622385ab1b68d9136e48ba51"),
"cost" : 78000
}
],
"cust_review" : [
{
"cust_id" : ObjectId("622387f51b68d9136e48ba55"),
"rating" : 5
}
]
}
{
"_id" : ObjectId("622389ae1b68d9136e48ba59"),
"ind_name" : "KK Tours",
"package" : [
{
"pkg_id" : 333,
"tour_id" : ObjectId("622385ce1b68d9136e48ba53"),
"cost" : 57000
},
{
"pkg_id" : 334,
"tour_id" : ObjectId("622385ab1b68d9136e48ba51"),
"cost" : 79000
}
],
"cust_review" : [
{
"cust_id" : ObjectId("622387f51b68d9136e48ba55"),
"rating" : 5
},
{
"cust_id" : ObjectId("622387ef1b68d9136e48ba54"),
"rating" : 4
}
]
}
> db.customer.find({}).pretty()
{
"_id" : ObjectId("622387ef1b68d9136e48ba54"),
"cust_name" : "Aniket",
"selected_pkg" : [
111
]
}
{
"_id" : ObjectId("622387f51b68d9136e48ba55"),
"cust_name" : "Nik",
"selected_pkg" : [
222,
333,
334
]
}
{
"_id" : ObjectId("622387fa1b68d9136e48ba56"),
"cust_name" : "Sham",
"selected_pkg" : [
111,
222
]
}
{
"_id" : ObjectId("62238c671b68d9136e48ba5a"),
"cust_name" : "John",
"selected_pkg" : [
111,
222,
223,
333,
334
]
}
I want to perform the following queries :
1] List all the details of expenses made by John on his first 3 trips.
Also display the total expenses
2] List the names of the customers who went on a tour to Shillong. [5]
It is possible to perform those queries on the given document or document structure is wrong?
I try this for 1st query
db.turism_ind.aggregate(
{$unwind : "$package"},
{$match : { "package.pkg_id" : {$in : [111,222,333] } } },
{$project :{ _id : 0 , package : 1, total_cost : {$sum : "$package.cost"} }}
)
Output (Not Correct)
{ "package" : { "pkg_id" : 111, "tour_id" : ObjectId("622385ba1b68d9136e48ba52"), "cost" : 85000 }, "total_cost" : 85000 }
{ "package" : { "pkg_id" : 222, "tour_id" : ObjectId("622385ba1b68d9136e48ba52"), "cost" : 82000 }, "total_cost" : 82000 }
{ "package" : { "pkg_id" : 333, "tour_id" : ObjectId("622385ce1b68d9136e48ba53"), "cost" : 57000 }, "total_cost" : 57000 }
Here is the aggregation for your first part. There might be some bug depending on your other documents inside your collections.
Here's a quick breakdown of your question.
List all the details of expenses made by John
Utilize $match to get john document
on his first 3 trips.
Utilize $project to slice the first 3 elements out and the do a $lookup on the turism_ind to grab the necessary documents
Also display the total expenses
Had to utilize the $reduce in a projection to flatten 2D array into 1D, and follow by another project to $sum all the cost together as total.
MongoDB Playground
db.customer.aggregate([
{
"$match": {
"cust_name": "John"
}
},
{
"$project": {
"firstThree": {
"$slice": [
"$selected_pkg",
0,
3
]
}
}
},
{
"$lookup": {
"from": "turism_ind",
"localField": "firstThree",
"foreignField": "package.pkg_id",
"as": "tour"
}
},
{
"$project": {
"_id": 0,
"packages": {
$reduce: {
input: "$tour.package",
initialValue: [],
in: {
$concatArrays: [
"$$value",
"$$this"
]
}
}
}
}
},
{
"$project": {
"packages": 1,
"total": {
"$sum": "$packages.cost"
}
}
}
])
Result
[
{
"packages": [
{
"cost": 85000,
"pkg_id": 111,
"tour_id": ObjectId("622385ba1b68d9136e48ba52")
},
{
"cost": 82000,
"pkg_id": 222,
"tour_id": ObjectId("622385ba1b68d9136e48ba52")
},
{
"cost": 78000,
"pkg_id": 223,
"tour_id": ObjectId("622385ab1b68d9136e48ba51")
}
],
"total": 245000
}
]

Navigate thought a lots of array and update value of object with mongo query

Hey guys I'm trying to update a value that is in an array in MongoDB, am trying to use the mongo queries but is not working, am following the next documentation from Mongo doc
this one is the array:
{
"_id" : "605e3d9b9ef219de662113d0",
"distribution" : [
{
"floor" : 1,
"rooms" : [
{
"number" : 301,
"beds" : [
{
"number" : 818,
"status" : "Vacante Sucia"
},
{
"number" : 819,
"status" : "Vacante Sucia"
}
],
"gender" : "M"
},
{
"number" : 302,
"beds" : [
{
"number" : 820,
"status" : "Vacante Sucia"
},
{
"number" : 821,
"status" : "Vacante Sucia"
}
],
"gender" : "M"
},
{
"number" : 303,
"beds" : [
{
"number" : 822,
"status" : "Vacante Sucia"
},
{
"number" : 823,
"status" : "Vacante Sucia"
}
],
"gender" : "M"
}
]
}
],
"name" : "Meteorologia",
"code" : "METEO"
}
this one is the query that is using in mongoDB to update the status from the bed 801, room 301, floor 1:
in the arrayFilters i specified the index 0 to get the first element of the arrays
db.getCollection('establishments_copy').findAndModify({query: { code: "METEO"}, update: { $set: { "distribution.$[i].rooms.$[i].beds.$[i].status": "TEST"}}, arrayFilters: [{"i.rooms": 0, "i.beds": 0, "i.status": 0}]})
they are returning me the collection but without changes, is possible to navigate validating not for the index just with the values.
for example using the next query:
db.getCollection('establishments_copy').findAndModify({query: { code: 'METEO', distribution: { $elemMatch: { floor: 1, 'rooms.number': 301, 'rooms.beds.number': 818}}}, update: { $set: { '...': 'CHANGED'}}})
thanks!
You just need to create separate condition as per sub document's field name,
f for floor field in distribution array
r for number field in rooms array
b for number field in beds array
db.getCollection('establishments_copy').findAndModify({
query: { code: "METEO"},
update: {
$set: {
"distribution.$[f].rooms.$[r].beds.$[b].status": "TEST"
}
},
arrayFilters: [
{ "f.floor": 1 },
{ "r.number": 301 },
{ "b.number": 818 }
]
})
Playground

Need help in querying mongodb

I have a a few documents that have the following structure. See attached image.
document structure
Each document includes an array of 'FileMeta' objects and each FileMeta object includes an array of 'StatusHistory' objects. I'm trying to get only the FileMetas that contain StatusCode equal to 4 and that the TimeStamp is greater than a certain datetime.
Tried the following query but it only returns the first FileMeta element of each document.
db.getCollection('Collection').find({'ExternalParams.RequestingApplication':'aaa.bbb'},
{ "FileMeta": { $elemMatch: { "StatusHistory":{ $elemMatch:{ "StatusCode": 4, "TimeStamp": { $gt: ISODate("2020-06-28T11:02:26.542Z")} } } } }} )
What am I doing wrong?
here is the document structure:
{
"_id" : ObjectId("5ef84e2ec08abf38b0043ab4"),
"FileMeta" : [
{
"StatusHistory" : [
{
"StatusCode" : 0,
"StatusDesc" : "New File",
"TimeStamp" : ISODate("2020-06-28T11:00:46.286Z")
},
{
"StatusCode" : 2,
"StatusDesc" : "stby",
"TimeStamp" : ISODate("2020-06-28T11:02:20.400Z")
},
{
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.937Z")
}
]
},
{
"StatusHistory" : [
{
"StatusCode" : 0,
"StatusDesc" : "New File",
"TimeStamp" : ISODate("2020-06-28T11:00:46.286Z")
},
{
"StatusCode" : 2,
"StatusDesc" : "stby",
"TimeStamp" : ISODate("2020-06-28T11:02:20.617Z")
},
{
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.542Z")
}
]
}
],
}
I want to return only the FileMeta objects that include a StatusHistory that match the following conditions: StatusCode = 4 and TimeStamp > SomeDateTime
Sorry for the delay, mate, I've been quite busy lately. Hope you already solved your problem. Anyway, I think that I found the solution.
As you can see on this link, the example shows that by default the $elemMatch operator returns the whole array in case of match on any element.
For instance, consider the following collection:
{ _id: 1, results: [ { product: "abc", score: 10 }, { product: "xyz", score: 5 } ] }
{ _id: 2, results: [ { product: "abc", score: 8 }, { product: "xyz", score: 7 } ] }
{ _id: 3, results: [ { product: "abc", score: 7 }, { product: "xyz", score: 8 } ] }
If you do the following query, for example:
db.survey.find(
{ results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } }
)
The output will be:
{ "_id" : 3, "results" : [ { "product" : "abc", "score" : 7 }, { "product" : "xyz", "score" : 8 } ] }
Not:
{ "_id" : 3, "results" : [{ "product" : "xyz", "score" : 8 }]}
That said, if you want to return only the document in the array that matches the specified query, you must use the db.collection.aggregate() function with the $unwind and $match operator.
The query below shall give you what you want.
Query:
db.collection.aggregate([
{"$unwind" : "$FileMeta"},
{"$unwind" : "$FileMeta.StatusHistory"},
{
"$match" : {
"FileMeta.StatusHistory.StatusCode" : 4,
"FileMeta.StatusHistory.TimeStamp" : {"$gte" : ISODate("2020-06-28T11:02:26.937Z")}
}
}
]).pretty()
Result:
{
"_id" : ObjectId("5ef84e2ec08abf38b0043ab4"),
"FileMeta" : {
"StatusHistory" : {
"StatusCode" : 4,
"StatusDesc" : "success",
"TimeStamp" : ISODate("2020-06-28T11:02:26.937Z")
}
}
}
One last tip. Consider changing your modeling to something that looks like the unwinded document, and remember that one document should be equivalent to one row in a normal relational database. So avoid storing information that should be on "several rows" on a single document.
Useful links:
The $elemMatch operator.
The $unwind operator.

MongoDB aggregation pipeline filtering two arrays

Could you give me advise? I have a document like this:
{
"_id" : ObjectId("569620270d3ac01895316edb"),
"customerId" : NumberLong("2000900000000000022"),
"gender" : "MALE",
"birthDate" : ISODate("1976-01-06T23:00:00Z"),
"someArray" : [
{
"id" : 5411,
"firstDate" : ISODate("2014-08-05T16:17:50Z"),
"lastDate" : ISODate("2015-10-31T11:55:51Z"),
"sumOfAll" : 5677.35,
"minAmount" : 9.75,
"maxAmount" : 231.72,
"innerArray" : [
{
"count" : 4,
"amount" : 449.33
},
{
"count" : 3,
"amount" : 401.31
},
{
"count" : 7,
"amount" : 617.8000000000001
},
{
"count" : 4,
"amount" : 465.28999999999996
},
{
"count" : 2,
"amount" : 212.95999999999998
},
{
"count" : 4,
"amount" : 497.53999999999996
},
{
"count" : 3,
"amount" : 278.23
},
{
"count" : 3,
"amount" : 383.15999999999997
},
{
"count" : 6,
"amount" : 459.63
},
{
"count" : 9,
"amount" : 677.19
},
{
"count" : 4,
"amount" : 393.85
}
]
},
{
"id" : 5812,
"firstDate" : ISODate("2014-09-03T17:16:32Z"),
"lastDate" : ISODate("2015-11-04T22:59:59Z"),
"sumOfAll" : 275.6,
"minAmount" : 15,
"maxAmount" : 69,
"innerArray" : [
{
"count" : 1,
"amount" : 17
},
{
"count" : 1,
"amount" : 15.4
},
{
"count" : 1,
"amount" : 69
},
{
"count" : 1,
"amount" : 53.7
},
{
"count" : 2,
"amount" : 84
}
]
},
{
"id" : 7399,
"firstDate" : ISODate("2015-01-12T22:59:59Z"),
"lastDate" : ISODate("2015-03-16T22:59:59Z"),
"sumOfAll" : 144.73,
"minAmount" : 0.84,
"maxAmount" : 24.98,
"innerArray" : [
{
"count" : 5,
"amount" : 50.379999999999995
},
{
"count" : 5,
"amount" : 55.45
},
{
"count" : 10,
"amount" : 38.900000000000006
}
]
},
]
}
And I'd like to filter both inner arrays and also project them. I'm trying this query:
db.sandbox.aggregate([
{ $match: {
'gender': {$eq : 'MALE'},
$or: [
{ $and: [{'someArray.id': {$eq: 5411}}, {'someArray.innerArray.count': 4}, {'someArray.innerArray.amount': {$gte: 2}}]},
{ $and: [{'someArray.id': {$eq: 5812}}, {'someArray.innerArray.count': 5}, {'someArray.innerArray.amount': {$gte: 50}}]},
]
}
},
{ $project: {
gender: 1,
customerId: 1,
someArray: { $filter: {
input: '$someArray',
as: 'item',
cond: {
$and: [
{ $or: [
{$and: [{$eq: ['$$item.id', 5411]}, {$eq: ['$$item.innerArray.count', 4]}, {$gte: ['$$item.innerArray.amount', 2]}]},
{$and: [{$eq: ['$$item.id', 5812]}, {$eq: ['$$item.innerArray.count', 5]}, {$gte: ['$$item.innerArray.amount', 50]}]},
]},
]
}
}},
}}
]).pretty()
And I received result without data in someArray:
{
"_id" : ObjectId("569620270d3ac01895316edb"),
"customerId" : NumberLong("2000900000000000022"),
"gender" : "MALE",
"someArray" : [ ]
}
I want to receive:
{
"_id" : ObjectId("569620270d3ac01895316edb"),
"customerId" : NumberLong("2000900000000000022"),
"gender" : "MALE",
"birthDate" : ISODate("1976-01-06T23:00:00Z"),
"someArray" : [
{
"id" : 5411,
"firstDate" : ISODate("2014-08-05T16:17:50Z"),
"lastDate" : ISODate("2015-10-31T11:55:51Z"),
"sumOfAll" : 5677.35,
"minAmount" : 9.75,
"maxAmount" : 231.72,
"innerArray" : [
{
"count" : 4,
"amount" : 449.33
},
{
"count" : 4,
"amount" : 465.28999999999996
},
{
"count" : 4,
"amount" : 497.53999999999996
},
{
"count" : 4,
"amount" : 393.85
}
]
}
]
}
If I change $eq to $gte, I will receive receive result, but I want to project innerArray too. How can I implement this? Should I use my own MapReduce job or I will able to do this with Aggregation pipeline?
MongoDB version 3.2. Also I observe when I'm trying to use several predicates for array and project only one element, for example:
db.sandbox.find( {$and: [{'someArray.id': 7399}, {'someArray.sumOfAll': {$gte: 5000}}]}, {'customerId': 1, 'someArray.$': 1}).pretty()
But it returns me:
{
"_id" : ObjectId("569620270d3ac01895316edb"),
"customerId" : NumberLong("2000900000000000022"),
"someArray" : [
{
"id" : 5411,
"firstDate" : ISODate("2014-08-05T16:17:50Z"),
"lastDate" : ISODate("2015-10-31T11:55:51Z"),
"sumOfAll" : 5677.35,
"minAmount" : 9.75,
"maxAmount" : 231.72,
"innerArray" : [
{
"count" : 4,
"amount" : 449.33
},
{
"count" : 3,
"amount" : 401.31
},
{
"count" : 7,
"amount" : 617.8000000000001
},
{
"count" : 4,
"amount" : 465.28999999999996
},
{
"count" : 2,
"amount" : 212.95999999999998
},
{
"count" : 4,
"amount" : 497.53999999999996
},
{
"count" : 3,
"amount" : 278.23
},
{
"count" : 3,
"amount" : 383.15999999999997
},
{
"count" : 6,
"amount" : 459.63
},
{
"count" : 9,
"amount" : 677.19
},
{
"count" : 4,
"amount" : 393.85
}
]
}
]
}
Which is incorrect for my perspective. I expect nothing.
First, the way you're using conditions in $match will not result in what you want.
{ $and: [{'someArray.id': {$eq: 5411}}, {'someArray.innerArray.count': 4}, {'someArray.innerArray.amount': {$gte: 2}}]}
The line above will verify each condition separately, instead of checking the count and amount conditions together for each innerArray element. If that's what you want, you should look into the $elemMatch operator.
Second, I don't believe you can use $filter like that on a second-level array. You should unwind someArray first:
db.sandbox.aggregate(
{
$match:
{
gender: { $eq: 'MALE' },
"someArray.id":
{
$in: [5411, 5812]
}
}
},
{
$unwind: "$someArray",
},
{
$project:
{
gender: 1,
customerId: 1,
someArray:
{
id: 1,
firstDate: 1,
lastDate: 1,
sumOfAll: 1,
minAmount: 1,
maxAmount: 1,
innerArray:
{
$filter:
{
input: '$someArray.innerArray',
as: 'item',
cond:
{
$or:
[
{
$and:
[
{ $eq: ['$$item.count', 4] },
{ $gte: ['$$item.amount', 2] }
]
},
{
$and:
[
{ $eq: ['$$item.count', 5] },
{ $gte: ['$$item.amount', 50] }
]
}
]
}
}
}
},
}
})
You can also $group someArray elements back if you want.

Resources