Aggregating a total from subdocuments in MongoDB - arrays

I have a document like the one below, I'd essentially like to produce an aggregate for the items in a sub document.
Essentially each document is a sales record, which has details of the sales and a sub document / array with the qtys of each item sold.
I'd like to produce a summary of all the items sold.
So an example collection is:
{
non_relevant_1: "ABC",
non_relevant_2: "DEF",
items_array: {
"item_1": 1,
"item_2": 2,
"item_3": 1,
"item_4": 1
}
},
{
non_relevant_1: "HIJ",
non_relevant_2: "KLM",
items_array: {
"item_1": 3,
"item_2": 2,
"item_3": 4
}
}
I'd then like to be able to produce something like:
{
items_array: {
"item_1": 4,
"item_2": 4,
"item_3": 5,
"item_4": 1
}
}
Many thanks in advance.

I think you need to change your schema, you are saving data in keys.
MongoDB operators are not made to have unknown keys, for example we can't group by an unknown key.To do those we do complicated and slow things like $objectToArray.
Also the data that you want as results have the same problem.
If you look at the query only the middle $unwind and $group would be needed it, with a changed schema, and asking for data without data in keys.
I mean instead of
items_array: {
"item_1": 1,
"item_2": 2,
"item_3": 1,
"item_4": 1
}
Your collection should have being like(first part of the query does that changing your schema)
items_array: [
{"name" "item_1",
"qty" : 1},
{"name" "item_2",
"qty" : 2},
{"name" "item_3",
"qty" : 1},
{"name" "item_4",
"qty" : 1}
]
Also the results should have known keys only.
Maybe the reason you were stuck is that.You will make things much easier for you.
Test code here
Query (query works, for your schema but i told you what i think)
db.collection.aggregate([
{
"$addFields": {
"items_array": {
"$map": {
"input": {
"$map": {
"input": {
"$objectToArray": "$items_array"
},
"as": "m",
"in": [
"$$m.k",
"$$m.v"
]
}
},
"as": "item",
"in": {
"name": {
"$arrayElemAt": [
"$$item",
0
]
},
"qty": {
"$arrayElemAt": [
"$$item",
1
]
}
}
}
}
}
},
{
"$unwind": {
"path": "$items_array"
}
},
{
"$group": {
"_id": "$items_array.name",
"total-qty": {
"$sum": "$items_array.qty"
}
}
},
{
"$group": {
"_id": null,
"items_array": {
"$push": {
"$map": {
"input": {
"$map": {
"input": {
"$objectToArray": "$$ROOT"
},
"as": "m",
"in": [
"$$m.k",
"$$m.v"
]
}
},
"as": "i",
"in": {
"$arrayElemAt": [
"$$i",
1
]
}
}
}
}
}
},
{
"$project": {
"_id": 0
}
},
{
"$addFields": {
"items_array": {
"$arrayToObject": "$items_array"
}
}
}
])

Related

Mongo Query to modify the existing field value with new value + array of objects

I want to update many documents based on the condition in MongoDB.
MODEL is the collection which has the document with below information.
"info": [
{
"field1": "String1",
"field2": "String2"
},
{
"field1": "String1",
"field2": "String_2"
}
],
"var": "x"
I need to update all the "String1" value of field1 with "STRING_NEW". I used the below query to update but not working as expected.
db.model.updateMany(
{ "info.field1": { $exists: true } },
[
{ "$set": {
"info": {
"$map": {
"input": "$info.field1",
"in": {
"$cond": [
{ "$eq": ["$$this.field1", "String1"] },
"STRING_NEW",
$$this.field1
]
}
}
}
} }
]
)
Please have a look and suggest if anything is to be modified in the above query.
Solution 1
With the update with aggregation pipeline, you should iterate the object in info array and update the iterated object by merging the current object with new field1 field via $mergeObjects.
db.model.updateMany({
"info.field1": "String1"
},
[
{
"$set": {
"info": {
"$map": {
"input": "$info",
"in": {
"$cond": [
{
"$eq": [
"$$this.field1",
"String1"
]
},
{
$mergeObjects: [
"$$this",
{
"field1": "STRING_NEW"
}
]
},
"$$this"
]
}
}
}
}
}
])
Demo Solution 1 # Mongo Playground
Solution 2
Can also work with $[<identifier>] positional filtered operator and arrayFilters.
db.model.updateMany({
"info.field1": "String1"
},
{
"$set": {
"info.$[info].field1": "STRING_NEW"
}
},
{
arrayFilters: [
{
"info.field1": "String1"
}
]
})
Demo Solution 2 # Mongo Playground

MongoDB query to return docs based on an array size after filtering array of JSON objects?

I have MongoDB documents structured in this way:
[
{
"id": "car_1",
"arrayProperty": [
{
"model": "sedan",
"turbo": "nil"
},
{
"model": "sedan",
"turbo": "60cc"
}
]
},
{
"id": "car_2",
"arrayProperty": [
{
"model": "coupe",
"turbo": "50cc"
},
{
"model": "coupe",
"turbo": "60cc"
}
]
}
]
I want to be able to make a find query that translates into basic English as "Ignoring all models that have 'nil' value for 'turbo', return all documents with arrayProperty of length X." That is to say, the "arrayProperty" of car 1 would be interpreted as having a size of 1, while the array of car 2 would have a size of 2. The goal is to be able to make a query for all cars with arrayProperty size of 2 and only see car 2 returned in the results.
Without ignoring the nil values, the query is very simple as:
{ arrayProperty: { $size: 2} }
And this would return both cars 1 and 2. Moreover, if our array was just a simple array such as:
[1,2,3,'nil]
Then our query is simply:
{
arrayProperty: {
$size: X,
$ne: "nil"
}
}
However, when we introduce an array of JSON objects, things get tricky. I have tried numerous things to no avail including:
"arrayProperty": {
$size: 2,
$ne: {"turbo": "nil"}
}
"arrayProperty": {
$size: 2,
$ne: ["arrayProperty.turbo": "nil"]
}
Even without the $size operator in there, I can't seem to filter by the nil value. Does anyone know how I would properly do this in those last two queries?
use $and in $match
db.collection.aggregate([
{
$match: {
"$and": [
{
arrayProperty: {
$size: 2
}
},
{
"arrayProperty.turbo": {
$ne: "nil"
}
}
]
}
}
])
mongoplayground
use $set first
db.collection.aggregate([
{
"$set": {
"arrayProperty": {
"$filter": {
"input": "$arrayProperty",
"as": "a",
"cond": {
$ne: [
"$$a.turbo",
"nil"
]
}
}
}
}
},
{
$match: {
arrayProperty: {
$size: 1
}
}
}
])
mongoplayground
set a new field of size
db.collection.aggregate([
{
"$set": {
"size": {
$size: {
"$filter": {
"input": "$arrayProperty",
"as": "a",
"cond": {
$ne: [
"$$a.turbo",
"nil"
]
}
}
}
}
}
},
{
$match: {
size: 1
}
}
])
mongoplayground

MongoDB: How do I retrieve a filtered set of results and a related collection that’s filtered in different ways before being counted?

I have a collection C1 which looks like this (simplified):
[
// ID and other irrelevant fields omitted
{
"name": "A",
"related": "Y",
"special": "foo",
"created" : ISODate("2020-02-07T19:36:52.757+02:00")
},
{
"name": "B",
"related": "Z",
"special": "bar",
"created" : ISODate("2020-02-07T19:36:52.757-06:00")
},
{
"name": "C",
"related": "X",
"special": "baz",
"created" : ISODate("2020-02-07T19:36:52.757+01:00")
},
{
"name": "D",
"related": "Z",
"special": "quux",
"created" : ISODate("2020-02-07T19:36:52.757+01:00")
},
// ...more records omitted...
]
And a collection C2 which looks like this (again, simplified):
[
// ID and other irrelevant fields omitted
{
"name": "X",
"total": 500
},
{
"name": "Y",
"total": 200
},
{
"name": "Z",
"total": 10
},
// ...more records omitted...
]
How can I, in a single query, retrieve a filtered set of records in C1 (e.g. { "special": "foo" }) with each record having a field c2 containing the matching records from C2 (C1.related being equal to C2.name) in addition to:
a field lowCount being the count of all matching records from C2 where { total: { $lte: 100 } }
a field midCount being the count of all matching records from C2 where { total: { $lte: 500, $gt: 100 } }
a field highCount being the count of all matching records from C2 where { total: { $gt: 500 } }
I realize that the database structure is awkward for what needs to be done, but I came in long after that was finalized and it can’t be overhauled at this point. The actual code is written in Java using Spring.
You need to use MongoDb aggregation.
db.c1.aggregate([
{
$match: {
"special": "foo"
}
},
{
$lookup: {
from: "c2",
localField: "related",
foreignField: "name",
as: "c2"
}
},
{
$addFields: {
lowCount: {
$size: {
$filter: {
input: "$c2",
cond: {
$lte: [
"$$this.total",
100
]
}
}
}
},
midCount: {
$size: {
$filter: {
input: "$c2",
cond: {
$lte: [
"$$this.total",
500
]
}
}
}
},
highCount: {
$size: {
$filter: {
input: "$c2",
cond: {
$gte: [
"$$this.total",
500
]
}
}
}
}
}
}
])
MongoPlayground
Spring Data allows to aggregate with MongoTemplate class (implements MongoOperations). Take a look how to transform this pipeline into Spring syntax here

Query only for numbers in nested array

I am trying to get an average number of an key in a nested array inside a document, but not sure how to accomplish this.
Here is how my document looks like:
{
"_id": {
"$oid": "XXXXXXXXXXXXXXXXX"
},
"data": {
"type": "PlayerRoundData",
"playerId": "XXXXXXXXXXXXX",
"groupId": "XXXXXXXXXXXXXX",
"holeScores": [
{
"type": "RoundHoleData",
"points": 2
},
{
"type": "RoundHoleData",
"points": 13
},
{
"type": "RoundHoleData",
"points": 3
},
{
"type": "RoundHoleData",
"points": 1
},
{
"type": "RoundHoleData",
"points": 21
}
]
}
}
Now, the tricky part of this is that I only want the average of points for holeScores[0] of all documents with this playerid and this groupid.
Actually, the best solution would be collecting all documents with playerid and groupid and create a new array with the average of holeScores[0], holeScores[1], holeScores[2]... But if I only can get one array key at the time, that would be OK to :-)
Here is what I am thinking but not quit sure how to put it together:
var allScores = dbCollection('scores').aggregate(
{$match: {"data.groupId": groupId, "playerId": playerId}},
{$group: {
_id: playerId,
rounds: { $sum: 1 }
result: { $sum: "$data.scoreTotals.points" }
}}
);
Really hoping for help with this issue and thanks in advance :-)
You can use $unwind with includeArrayIndex to get index and then use $group to group by that index
dbCollection('scores').aggregate(
{
$match: { "data.playerId": "XXXXXXXXXXXXX", "data.groupId": "XXXXXXXXXXXXXX" }
},
{
$unwind: {
path: "$data.holeScores",
includeArrayIndex: "index"
}
},
{
$group: {
_id: "$index",
playerId: { $first: "data.playerId" },
avg: { $avg: "$data.holeScores.points" }
}
}
)
You can try below aggregation
db.collection.aggregate(
{ "$match": { "data.groupId": groupId, "data.playerId": playerId }},
{ "$group": {
"_id": null,
"result": {
"$sum": {
"$arrayElemAt": [
"$data.holeScores.points",
0
]
}
}
}}
)

Mongoose - How to retrieve an array of objects from each document in collection?

Each document in my collection looks something like this (JSON):
{
name: "Roger",
matches: [
{
opponent: "Rafael Nadal",
match_date: 1494536400000
},
{
opponent: "Nick Kyrgyos",
match_date: 1494557400000
}
]
}
I want to extract all the matches each player had and also sort them by match_date using Mongoose, but I don't know how.
Could you please help me with this?
You can try below aggregation
db.collection.aggregate([
{ "$unwind": "$matches" },
{ "$sort": { "matches.match_date": 1 }},
{ "$group": {
"_id": "$_id",
"matches": { "$push": "$matches" }
}},
{ "$limit": 20 },
{ "$project": { "matches": 1, "numberOfMatches": { "$size": "$matches" } }}
])

Resources