MongoDB aggregation but not including certain items - mongodb

I'm very new to MongoDB's aggregation framework, so I do not know properly how to do this.
I have a data model that is structured like this:
{
name: String,
store: {
item1: Number,
item2: Number,
item3: Number,
item4: Number,
},
createdAt: Date
}
I want to return the average price of every item'i'. I'm trying with this query:
db.commerces.aggregate([
{
$group: {
_id: "",
item1Avg: { $avg: "$store.item1"},
item2Avg: { $avg: "$store.item2"},
item3Avg: { $avg: "$store.item3"},
item4Avg: { $avg: "$store.item4"}
}
}
]);
The problem is that when an item has no price set, it's stored in the database as a "-1".
I don't want these values to pollute the average result. Is there any way to limit the agreggation to only take into account when price is > 0.
$match operator before $group is not a solution because I want to return all the average prices.
Thank you!
EDIT: Here you have of an example of the input & desired output:
[{
name: 'name',
store: {
item1: 10,
item2: -1,
item3: 12,
item4: 3,
}
},
{
name: 'name2',
store: {
item1: 10,
item2: -1,
item3: -1,
item4: 2,
}
},...]
An the desired output:
{
item1Avg: 10,
item2Avg: 0,
item3Avg: 12,
item4Avg: 2.5
}

You need to $unwind the store, then $match values to meet your condition, then $group ones that passed the test. Unfortunately there is no way to $unwind an object, so you need to $project it to array first:
db.commerces.aggregate([
{$project: {store:[
{item:{$literal:"item1"}, val:"$store.item1"},
{item:{$literal:"item2"}, val:"$store.item2"},
{item:{$literal:"item3"}, val:"$store.item3"},
{item:{$literal:"item4"}, val:"$store.item4"}
]}},
{$unwind:"$store"},
{$match: {"store.val":{$gt:0}}},
{$group: {_id:"$store.item", avg:{$avg:"$store.val"}}}
])
EDIT:
As #blakes-seven pointed, it may not work on versions < 3.2. An alternative approach with $map may work:
db.commerces.aggregate([
{$project: {
store: {
$map:{
input:[
{item:{$literal:"item1"}, val:"$store.item1"},
{item:{$literal:"item2"}, val:"$store.item2"},
{item:{$literal:"item3"}, val:"$store.item3"},
{item:{$literal:"item4"}, val:"$store.item4"}
],
as: "i",
in: "$$i"
}
}
}},
{$unwind:"$store"},
{$match: {"store.val":{$gt:0}}},
{$group: {_id:"$store.item", avg:{$avg:"$store.val"}}}
])

Related

Aggregate nested array element

I have troubles with aggregations. I have collection "stations" with theese fiels:
stationName: string,
systemName: string,
commodities:[{
name: string,
buyPrice: number,
sellPrice: number,
stock: number,
demand: number
}]
I need to query max and min price for specific commodity. For example: I have commodity "water" and need to get highest sellPrice at all stations entries.
Thanks in advance
You could try doing an aggregate query with $max/$min,
db.collection.aggregate([
{
$project: {
stationName: true,
systemName: true,
maxBuyPrice: {
$max: "$commodities.buyPrice"
},
maxSellPrice: {
$max: "$commodities.sellPrice"
},
}
}
])
Mongo playground.
For the min price you can replace $max with $min in the query, or include both if that's what you want
db.stations.aggregate([
{ $unwind: "$commodities"},
{ $match: { "commodities.name":"clothing" }}, //clothing as example
{ $sort: { "commodities.buyPrice":-1 }},
{ $limit: 10}
])
This worked for me.

How to retrieve specific keys when grouping on mongo while using $max on a field?

How can i retrieve keys beyond the grouped ones from mongodb?
Documents example:
{code: 'x-1', discount_value: 10, type: 1}
{code: 'x-2', discount_value: 8, type: 1}
{code: 'x-3', discount_value: 5, type: 2}
Query:
{
$match: {
type: 1
}
},
{
$group: {
_id: null
discount_value: {$max: '$discount_value'}
}
}
This query will retrieve the max value from discount_value (10) key and the key _id but how i can do to retrieve the code and type key as well if i don't have operation to do those keys?
The current result:
{_id: null, discount_value: 10}
Expected result:
{_id: null, discount_value: 10, type: 1, code: 'x-1'}
You can try below query :
db.collection.aggregate([
{
$match: { type: 1 }
},
{
$group: {
_id: null,
doc: {
$max: {
discount_value: "$discount_value",
type: "$type",
code: "$code"
}
}
}
}
])
I believe it would get $max on field discount_value and get respective type & code values from the doc where discount_value is max.
In another way, since you're using $match as first stage, I believe your data will be less enough to perform $sort efficiently :
db.collection.aggregate([
{
$match: { type: 1 }
},
{
$sort: { discount_value: -1 } // sort in desc order
},
{
$limit: 1
}
])
Test : mongoplayground
Note :
Test the first query on DB itself rather than in playground. In first query you can use $replaceRoot as last stage if you wanted to make doc field as root of your document.

MongoDB aggregation: $unwind after grouping by date

I have this model for purchases:
{
purchase_date: 2018-03-11 00:00:00.000,
total_cost: 400,
items: [
{
title: 'Pringles',
price: 200,
quantity: 2,
category: 'Snacks'
}
]
}
What I'm trying to do is to, first of all, to group the purchases by date, by doing so:
{$group: {
_id: {
date: $purchase_date,
items: '$items'
}
}}
However, now what I want to do is group the purchases of each day by items[].category and calculate how much was spent for each category in that day. I was able to do that with one day, but when I grouped each purchase by date I no longer able to $unwind the items.
I tried passing the path $items and it doesn't find it at all. If I try to use $_id.$items or _id.$items in both cases I get an error stating that it is not a valid path for $unwind.
You can use purchase_data and items.category as a grouping _id but you need to use $unwind on items before and then you can add another $group to get all groups per day
db.col.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: {
purchase_date: "$purchase_date",
category: "$items.category",
},
total: { $sum: { $multiply: [ "$items.price", "$items.quantity" ] } }
}
},
{
$group: {
_id: "$_id.purchase_date",
categories: { $push: { name: "$_id.category", total: "$total" } }
}
}
])

Counting data per user with mongo aggregation framework

I have a collection, where each document contains user_ids as a property, which is an Array field. Example document(s) would be :
[{
_id: 'i3oi1u31o2yi12o3i1',
unique_prop: 33,
prop1: 'some string value',
prop2: 212,
user_ids: [1, 2, 3 ,4]
},
{
_id: 'i3oi1u88ffdfi12o3i1',
unique_prop: 34,
prop1: 'some string value',
prop2: 216,
user_ids: [2, 3 ,4]
},
{
_id: 'i3oi1u8834432ddsda12o3i1',
unique_prop: 35,
prop1: 'some string value',
prop2: 211,
user_ids: [2]
}]
My goal is to get number of documents per user, so sample output would be :
[
{user_id: 1, count: 1},
{user_id: 2, count: 3},
{user_id: 3, count: 2},
{user_id: 4, count: 2}
]
I've tried couple of things none of which worked, lastly I tried :
aggregate([
{ $group: {
_id: { unique_prop: "$unique_prop"},
users: { "$addToSet": "$user_ids" },
count: { "$sum": 1 }
}}
]
But it just returned the users per document. I m still trying to learn the any resource or advice would help.
You need to $unwind the "user_ids" array and in the $group stage count the number of time each "id" appears in the collection.
db.collection.aggregate([
{ "$unwind": "$user_ids" },
{ "$group": { "_id": "$user_ids", "count": {"$sum": 1 }}}
])
MongoDB aggregation performs computation on group of values from documents in a collection and return computed result through executing its stages in a pipeline.
According to above mentioned description please try executing following aggregate query in MongoDB shell.
db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$unwind: "$user_ids"
},
// Stage 2
{
$group: {
_id:{user_id:'$user_ids'},
total:{$sum:1}
}
},
// Stage 3
{
$project: {
_id:0,
user_id:'$_id.user_id',
count:'$total'
}
},
]
);
In above aggregate query initially $unwind operator breaks an array field user_ids of each document into multiple documents for each element of array field and then it groups documents by value of user_ids field contained into each document and performs summation of documents for each value of user_ids field.

Sum unique properties in different collection elements

I am quite new to MongoDB. Hopefully I am using the correct terminology to express my problem.
I have the following collection:
Data collection
{
"name":"ABC",
"resourceId":"i-1234",
"volumeId":"v-1234",
"data":"11/6/2013 12AM",
"cost": 0.5
},
{
"name":"ABC",
"resourceId":"v-1234",
"volumeId":"",
"data":"11/6/2013 2AM",
"cost": 1.5
}
I want to query the collection such that if a volumeId matches with another entries resourceId, then sum up the corresponding resourceId's cost together.
As a result, the cost would be 2.0 in this case.
Basically I want to match the volumeId of one entry to the resourceId of another entry and sum the costs if matched.
I hope I have explained my problem properly. Any help is appreciated. Thanks
Try this aggregation query:
db.col.aggregate([
{
$project: {
resourceId: 1,
volumeId: 1,
cost: 1,
match: {
$cond: [
{$eq: ["$volumeId", ""]},
"$resourceId",
"$volumeId"
]
}
}
},
{
$group: {
_id: '$match',
cost: {$sum: '$cost'},
resId: {
$addToSet: {
$cond: [
{$eq: ['$match', '$resourceId']},
null,
'$resourceId'
]
}
}
}
},
{$unwind: '$resId'},
{$match: {
resId: {
$ne: null
}
}
},
{
$project: {
resourseId: '$resId',
cost: 1,
_id: 0
}
}
])
And you will get the following:
{ "cost" : 2, "resourseId" : "i-1234" }
This is assuming the statement I wrote in the comment is true.