Is this query possible in MongoDB? - mongodb

Imagine a data set like this:
db.test.insertMany([
{ '_id':1, 'name':'aa1', 'price':10, 'quantity': 2, 'category': ['coffe'] },
{ '_id':2, 'name':'aa2', 'price':20, 'quantity': 1, 'category': ['coffe', 'snack'] },
{ '_id':3, 'name':'aa3', 'price':5, 'quantity':10, 'category': ['snack', 'coffe'] },
{ '_id':4, 'name':'aa4', 'price':5, 'quantity':20, 'category': ['coffe', 'cake'] },
{ '_id':5, 'name':'aa5', 'price':10, 'quantity':10, 'category': ['animal', 'dog'] },
{ '_id':6, 'name':'aa6', 'price':5, 'quantity': 5, 'category': ['dog', 'animal'] },
{ '_id':7, 'name':'aa7', 'price':5, 'quantity':10, 'category': ['animal', 'cat'] },
{ '_id':8, 'name':'aa8', 'price':10, 'quantity': 5, 'category': ['cat', 'animal'] },
]);
I'm trying to make a query with this result (or something like it):
[
{ ['animal', 'dog'], 125 },
{ ['animal', 'cat'], 100 },
{ ['coffe', 'cake'], 100 },
{ ['coffe', 'snack'], 70 },
{ ['coffe'], 20 }
]
Meaning that it is:
Grouped by category.
The category is treated as a set (i.e. order is not important).
The result is sorted by price*quantity per unique category 'set'.
I've tried everything I know (which is very limited) and googled for days without getting anywhere.
Is this even possible in an aggregate query or do I have find a different way?

I suppose you need something like this:
db.collection.aggregate([
{
$unwind: "$category"
},
{
$sort: {
_id:-1,
category: -1
}
},
{
$group: {
_id: "$_id",
category: {
$push: "$category"
},
price: {
$first: "$price"
},
quantity: {
$first: "$quantity"
}
}
},
{
$group: {
_id: "$category",
sum: {
$sum: {
$multiply: [
"$price",
"$quantity"
]
}
}
}
},
{
$project: {
mySet: "$_id",
total: "$sum"
}
},
{
$sort: {
total: -1
}
}
])
Explained:
$unwind the $category array so you can sort the categories in same order.
$sort by category & _id so you can have same order per category & _id
$group by _id so you can push the categories back to array but sorted
$group by category set so you can sum the price*quantity
$project the needed fields
$sort by descending order as requested.
Please, note output has name for the set and total for the sum to be valid JSON since it is not possible to have the output as {[X,Y],Z} and need to be {m:[X,Y],z:Z}
playground

db.collection.aggregate([
{
"$match": {}
},
{
"$group": {
"_id": {
$function: {
body: "function(arr) { return arr.sort((a,b) => a.localeCompare(b))}",
args: [ "$category" ],
lang: "js"
}
},
"sum": {
"$sum": { "$multiply": [ "$price", "$quantity" ] }
}
}
},
{
"$sort": { sum: -1 }
}
])
mongoplayground
In mongodb 5.2 version you can use $sortArray instead of function sort that I used.

Related

Mongodb- group an array by key

I have an array field (contains objects) in multiple documents, I want to merge the arrays into one array and group the array by object key. I have manage to group the array but I dont know how to group the data. See the code I tried below
const test = await salesModel.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: 0,
data: { $addToSet: '$items' }
},
}
])
Result of the query:
{
_id: 0,
data: [
{
_id: 61435b3c0f773abaf77a367e,
price: 3000,
type: 'service',
sellerId: 61307abca667678553be81cb,
},
{
_id: 613115808330be818abaa613,
price: 788,
type: 'product',
sellerId: 61307abca667678553be81cb,
},
{
_id: 61307c1ea667676078be81cc,
price: 1200,
type: 'product',
sellerId: 61307abca667678553be81cb,
}
]
}
Now I want to group the data array by object key data.sellerId and sum price
Desired Output:
{
data: [
{
sumPrice: 788,
sellerId: 613115808330be818abaa613,
},
{
sumPrice: 1200,
sellerId: 61307abca667678553be81cb,
}
]
}
Extend with the current query and result with:
$unwind: Deconstruct the array field to multiple documents.
$group: Group by data.sellerId to sum ($sum) for data.price.
$group: Group by 0 with $addToSet to combine multiple documents into one document with data.
MongoDB aggregation query
db.collection.aggregate([
{
$unwind: "$data"
},
{
$group: {
_id: {
sellerId: "$data.sellerId"
},
"sumPrice": {
$sum: "$data.price"
}
}
},
{
"$group": {
"_id": 0,
"data": {
$addToSet: {
"sellerId": "$_id.sellerId",
"sumPrice": "$sumPrice"
}
}
}
}
])
Sample Mongo Playground
Output
[
{
"_id": 0,
"data": [
{
"sellerId": ObjectId("61307abca667678553be81cb"),
"sumPrice": 4988
}
]
}
]
If you want to re-write the query, here are the query with sample input.
Input
[
{
items: [
{
_id: ObjectId("61435b3c0f773abaf77a367e"),
price: 3000,
type: "service",
sellerId: ObjectId("61307abca667678553be81cb"),
},
{
_id: ObjectId("613115808330be818abaa613"),
price: 788,
type: "product",
sellerId: ObjectId("61307abca667678553be81cb"),
},
{
_id: ObjectId("61307c1ea667676078be81cc"),
price: 1200,
type: "product",
sellerId: ObjectId("61307abca667678553be81cb"),
}
]
}
]
Mongo aggregation query
db.collection.aggregate([
{
$unwind: "$items"
},
{
$group: {
_id: {
sellerId: "$items.sellerId"
},
"sumPrice": {
$sum: "$items.price"
}
}
},
{
"$group": {
"_id": 0,
"data": {
$addToSet: {
"sellerId": "$_id.sellerId",
"sumPrice": "$sumPrice"
}
}
}
}
])
Sample 2 on Mongo Playground
Output
[
{
"_id": 0,
"data": [
{
"sellerId": ObjectId("61307abca667678553be81cb"),
"sumPrice": 4988
}
]
}
]

MongoDB get size of unwinded array

I'm trying to return size of 'orders' and sum of 'item' values for each 'order' for each order from documents like the example document:
orders: [
{
order_id: 1,
items: [
{
item_id: 1,
value:100
},
{
item_id: 2,
value:200
}
]
},
{
order_id: 2,
items: [
{
item_id: 3,
value:300
},
{
item_id: 4,
value:400
}
]
}
]
I'm using following aggregation to return them, everything works fine except I can't get size of 'orders' array because after unwind, 'orders' array is turned into an object and I can't call $size on it since it is an object now.
db.users.aggregate([
{
$unwind: "$orders"
},
{
$project: {
_id: 0,
total_values: {
$reduce: {
input: "$orders.items",
initialValue: 0,
in: { $add: ["$$value", "$$this.value"] }
}
},
order_count: {$size: '$orders'}, //I get 'The argument to $size must be an array, but was of type: object' error
}
},
])
the result I expected is:
{order_count:2, total_values:1000} //For example document
{order_count:3, total_values:1500}
{order_count:5, total_values:2500}
I found a way to get the results that I wanted. Here is the code
db.users.aggregate([
{
$project: {
_id: 1, orders: 1, order_count: { $size: '$orders' }
}
},
{ $unwind: '$orders' },
{
$project: {
_id: '$_id', items: '$orders.items', order_count: '$order_count'
}
},
{ $unwind: '$items' },
{
$project: {
_id: '$_id', sum: { $sum: '$items.value' }, order_count: '$order_count'
}
},
{
$group: {
_id: { _id: '$_id', order_count: '$order_count' }, total_values: { $sum: '$sum' }
}
},
])
output:
{ _id: { _id: ObjectId("5dffc33002ef525620ef09f1"), order_count: 2 }, total_values: 1000 }
{ _id: { _id: ObjectId("5dffc33002ef525620ef09f2"), order_count: 3 }, total_values: 1500 }

Mongoose subquery

I have a collection that looks like below:
[
{
"orderNum": "100",
"createdTime": ISODate("2020-12-01T21:00:00.000Z"),
"amount": 100,
"memo": "100memo",
"list": [
1
]
},
{
"orderNum": "200",
"createdTime": ISODate("2020-12-01T21:01:00.000Z"),
"amount": 200,
"memo": "200memo",
"list": [
1,
2
]
},
{
"orderNum": "300",
"createdTime": ISODate("2020-12-01T21:02:00.000Z"),
"amount": 300,
"memo": "300memo"
},
{
"orderNum": "400",
"createdTime": ISODate("2020-12-01T21:03:00.000Z"),
"amount": 400,
"memo": "400memo"
},
]
and I'm trying to get the total amount of orders that were created before order# 300 (so order#100 and #200, total amount is 300).
Does anyone know how to get it via Mongoose?
You can use this one:
db.collection.aggregate([
{ $sort: { orderNum: 1 } }, // by default the order of documents in a collection is undetermined
{ $group: { _id: null, data: { $push: "$$ROOT" } } }, // put all documents into one document
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } }, // cut desired elementes from array
{ $unwind: "$data" }, // transform back to documents
{ $replaceRoot: { newRoot: "$data" } },
{ $group: { _id: null, total_amount: { $sum: "$amount" } } } // make summary
])
Actually it is not needed to $unwind and $group, so the shortcut would be this:
db.collection.aggregate([
{ $sort: { orderNum: 1 } },
{ $group: { _id: null, data: { $push: "$$ROOT" } } },
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $project: { total_amount: { $sum: "$data.amount" } } }
])
But the answer from #turivishal is even better.
Update for additional field
{
$set: {
data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] },
memo: { $arrayElemAt: [ "$data.memo", { $indexOfArray: ["$data.orderNum", "300"] } ] }
}
}
or
{ $set: { data: { $slice: ["$data", { $indexOfArray: ["$data.orderNum", "300"] }] } } },
{ $set: { memo: { $last: { "$data.memo" } } },
$match orderNum less than 300
$group by null and get totalAmount using $sum of amount
YourSchemaModel.aggregate([
{ $match: { orderNum: { $lt: "300" } } },
{
$group: {
_id: null,
totalAmount: { $sum: "$amount" }
}
}
])
Playground

Top documents per bucket

I would like to get the documents with the N highest fields for each of N categories. For example, the posts with the 3 highest scores from each of the past 3 months. So each month would have 3 posts that "won" for that month.
Here is what my work so far has gotten, simplified.
// simplified
db.posts.aggregate([
{$bucket: {
groupBy: "$createdAt",
boundaries: [
ISODate('2019-06-01'),
ISODate('2019-07-01'),
ISODate('2019-08-01')
],
default: "Other",
output: {
posts: {
$push: {
// ===
// This gets all the posts, bucketed by month
score: '$score',
title: '$title'
// ===
}
}
}
}},
{$match: {_id: {$ne: "Other"}}}
])
I attempted to use the $slice operator in between the // ===s, but go an error (below).
postResults: {
$each: [{
score: '$score',
title: '$title'
}],
$sort: {score: -1},
$slice: 2
}
An object representing an expression must have exactly one field: { $each: [ { score: \"$score\", title: \"$title\" } ], $sort: { baseScore: -1.0 }, $slice: 2.0 }
$slice you're trying to use is dedicated for update operations. To get top N posts you need to run $unwind, then $sort and $group to get ordered array. As a last step you can use $slice (aggregation), try:
db.posts.aggregate([
{$bucket: {
groupBy: "$createdAt",
boundaries: [
ISODate('2019-06-01'),
ISODate('2019-07-08'),
ISODate('2019-08-01')
],
default: "Other",
output: {
posts: {
$push: {
score: '$score',
title: '$title'
}
}
}
}},
{ $match: {_id: {$ne: "Other"}}},
{ $unwind: "$posts" },
{ $sort: { "posts.score": -1 } },
{ $group: { _id: "$_id", posts: { $push: { "score": "$posts.score", "title": "$posts.title" } } } },
{ $project: { _id: 1, posts: { $slice: [ "$posts", 3 ] } } }
])

total of all groups totals using mongodb

i did this Aggregate pipeline , and i want add a field contains the Global Total of all groups total.
{ "$match": query },
{ "$sort": cursor.sort },
{ "$group": {
_id: { key:"$paymentFromId"},
items: {
$push: {
_id:"$_id",
value:"$value",
transaction:"$transaction",
paymentMethod:"$paymentMethod",
createdAt:"$createdAt",
...
}
},
count:{$sum:1},
total:{$sum:"$value"}
}}
{
//i want to get
...project groups , goupsTotal , groupsCount
}
,{
"$skip":cursor.skip
},{
"$limit":cursor.limit
},
])
you need to use $facet (avaialble from MongoDB 3.4) to apply multiple pipelines on the same set of docs
first pipeline: skip and limit docs
second pipeline: calculate total of all groups
{ "$match": query },
{ "$sort": cursor.sort },
{ "$group": {
_id: { key:"$paymentFromId"},
items: {
$push: "$$CURRENT"
},
count:{$sum:1},
total:{$sum:"$value"}
}
},
{
$facet: {
docs: [
{ $skip:cursor.skip },
{ $limit:cursor.limit }
],
overall: [
{$group: {
_id: null,
groupsTotal: {$sum: '$total'},
groupsCount:{ $sum: '$count'}
}
}
]
}
the final output will be
{
docs: [ .... ], // array of {_id, items, count, total}
overall: { } // object with properties groupsTotal, groupsCount
}
PS: I've replaced the items in the third pipe stage with $$CURRENT which adds the whole document for the sake of simplicity, if you need custom properties then specify them.
i did it in this way , project the $group result in new field doc and $sum the sub totals.
{
$project: {
"doc": {
"_id": "$_id",
"total": "$total",
"items":"$items",
"count":"$count"
}
}
},{
$group: {
"_id": null,
"globalTotal": {
$sum: "$doc.total"
},
"result": {
$push: "$doc"
}
}
},
{
$project: {
"result": 1,
//paging "result": {$slice: [ "$result", cursor.skip,cursor.limit ] },
"_id": 0,
"globalTotal": 1
}
}
the output
[
{
globalTotal: 121500,
result: [ [group1], [group2], [group3], ... ]
}
]