Lets say i have a collection of books like this :
{author:"john", category:"action", title:"foobar200"},
{author:"peter", category:"scifi" , title:"42test"},
{author:"peter", category:"novel", title:"whatever_t"},
{author:"jane", category:"novel", title:"the return"},
{author:"john", category:"action", title:"extreme test"},
{author:"peter", category:"scifi", title:"such title"},
{author:"jane", category:"action", title:"super book "}
I want to do a query similar to :
SELECT author,category, count(*) FROM books GROUP BY category, author
==> result :
john -> action -> 2
john -> novel -> 0
john -> scifi -> 0
jane -> action -> 1
etc...
the closest i've been to the solution is this :
db.books.aggregate(
{
$match: {category:"action"}
},
{
$group: { _id: '$author', result: { $sum: 1 } }
}
);
==> result
{ "_id" : "jane", "result" : 1 }
{ "_id" : "john", "result" : 2 }
{ "_id" : "peter", "result" : 0 }
But i can't understand how to perform the second "group by" with categories.
What is the best way to do this ?
Thanks
You can include multiple fields in the _id used by $group to provide multi-field grouping:
db.books.aggregate([
{$group: {
_id: {category: '$category', author: '$author'},
result: {$sum: 1}
}}
])
Result:
{
"_id" : {
"category" : "action",
"author" : "jane"
},
"result" : 1
},
{
"_id" : {
"category" : "novel",
"author" : "jane"
},
"result" : 1
},
{
"_id" : {
"category" : "novel",
"author" : "peter"
},
"result" : 1
},
{
"_id" : {
"category" : "scifi",
"author" : "peter"
},
"result" : 2
},
{
"_id" : {
"category" : "action",
"author" : "john"
},
"result" : 2
}
Related
FULL DISCLOSURE: I'm a MongoDB noob
I'm dealing with a legacy DB structure. A part of my MongoDB looks like this currently:
Events (_id, name (string), ...)
Orders (_id, eventId (as string), products (array of {prodIdentifier (string), quantity (number)}), customer_ID (string), signee (string), sign_time (date), ...)
Products (_id, prodIdentifier (string), price (number), sku (string), ...)
The relations are as follows:
Event 1..N Orders (via eventId)
Orders 1..N Products (via products array)
I need to query in a way that given an eventId, I return
Order ID Customer Name (can be a cascade request / premeditated
by frontend), Product SKU, Product Name, Quantity,
Value (quantity * price), Signee Name, Sign time
Mind that, my interface requires filters and sorts on all of the above fields along with limit and offset for pagination, to reduce query time, fast UI, etc.
I could use populate on orders, but how am I supposed to honor the limit and offset via mongoose then. I'm wondering if I should make a view, in which case how should I flatten it to send/receive a list that honors the limit and offset.
Or will it have to be a very manual, step-by-step build of the resulting list?
UPDATE:
Sample data in the DB:
Event Object:
{
"_id" : ObjectId("6218b9266487367ba1c20258"),
"name" : "XYZ",
"createdAt" : ISODate("2022-02-03T13:25:43.814+0000"),
"updatedAt" : ISODate("2022-02-14T09:34:47.819+0000"),
...
}
Order(s):
[
{
"_id" : ObjectId("613ae653d0112f6b49fdd437"),
"orderItems" : [
{
"quantity" : NumberInt(2),
"productCode" : "VEO001",
},
{
"quantity" : NumberInt(2),
"productCode" : "VEO002",
},
{
"quantity" : NumberInt(1),
"productCode" : "VEO003",
}
],
"orderCode" : "1000",
"customerCode" : "Customer 1",
"createdAt" : ISODate("2021-09-10T05:00:03.496+0000"),
"updatedAt" : ISODate("2022-02-08T10:06:42.255+0000"),
"eventId" : "6218b9266487367ba1c20258"
}
]
Products:
[
{
"_id" : ObjectId("604206685f25b8560a1cd48d"),
"Product name" : "ABC",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO001",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "ABC",
"_costprice" : NumberInt(12),
"_sku" : "SKUVEO001",
},
{
"_id" : ObjectId("604206685f25b8560a1cd48a"),
"Product name" : "DEF",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO002",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "DEF",
"_costprice" : NumberInt(13),
"_sku" : "SKUVEO002",
},
{
"_id" : ObjectId("604206685f25b8560a1cd48a"),
"Product name" : "GHI",
"createdAt" : ISODate("2021-03-05T10:22:32.085+0000"),
"tag" : "VEO003",
"updatedAt" : ISODate("2022-03-28T07:29:21.939+0000"),
"Product Price" : NumberInt(0),
"photo" : {
"_id" : ObjectId("6042071a5f25b8560a1cd4a9"),
"key" : "e8c9a085-4e8d-4ac4-84e9-bb0a83a59145",
"name" : "Screenshot 2021-03-05 at 11.24.50.png"
},
"name" : "GHI",
"_costprice" : NumberInt(13),
"_sku" : "SKUVEO003",
},
]
Expected output:
You can do something like:
db.orders.aggregate([
{$match: {eventId: "6218b9266487367ba1c20258"}},
{
$lookup: {
from: "products",
localField: "orderItems.productCode",
foreignField: "tag",
as: "orderItemsB"
}
},
{
"$addFields": {
"orderItems": {
"$map": {
"input": "$orderItemsB",
"in": {
"$mergeObjects": [
"$$this",
{
"$arrayElemAt": [
"$orderItems",
{"$indexOfArray": ["$orderItems.productCode", "$$this.tag"]}
]
}
]
}
}
},
orderItemsB: 0
}
},
{
$unset: "orderItemsB"
},
{
$lookup: {
from: "events",
let: {eventId: "$eventId"},
pipeline: [
{
$match: {$expr: {$eq: [{$toString: "$_id"}, "$$eventId"]}}
}
],
as: "event"
}
},
{
$set: {event: {"$arrayElemAt": ["$event", 0]}}
},
{$unwind: "$orderItems"}
])
As you can see on this playground example. This will give you a document for each product of the order with all the data.
I want to group my data on the base of factoryId field and then each factory there will be multiple orders want to again group on basis of orderId as each order can contain multiple items. Here I am giving the example of my data and what I need and first group by which I tried.
{
"_id" : ObjectId("5b3e270c42d8004cea382e87"),
"factoryId" : ObjectId("5aa76190cef23a1561b8056c"),
"productId" : ObjectId("5aa78c66cef23a1561b80893"),
"orderId" : ObjectId("5b3e270c42d8004cea382e86"),
"generatedOrderId" : "3985-166770-4554",
"productName" : "Lakme Lotion"
},
{
"_id" : ObjectId("5b3e270c42d8004cea382e88"),
"factoryId" : ObjectId("5b39aed32832f72062e51c23"),
"productId" : ObjectId("5b3cb96139cec8341df52c4b"),
"orderId" : ObjectId("5b3e270c42d8004cea382e86"),
"generatedOrderId" : "3985-166770-4554",
"productName" : "Coke"
},
{
"_id" : ObjectId("5b3e27b07fe0d94d62b76b2a"),
"factoryId" : ObjectId("5aa76190cef23a1561b8057c"),
"productId" : ObjectId("5ac21075ac347a5fbf355028"),
"orderId" : ObjectId("5b3e27b07fe0d94d62b76b27"),
"generatedOrderId" : "3985-755507-7484",
"productName" : "Spoon"
}
And I want result as:
{
"factoryId":ObjectId("5aa76190cef23a1561b8057c"),
"orders":[
{
"orderId":ObjectId("5b3e270c42d8004cea382e86")
"items":[
{
"productName":"Lakme Lotion"
},
{
"productName":"Coke"
}
]
}
]
}
Can anyone help me with this?. Any help is appreciated.
I tried and It worked for me. Sorry
db.getCollection("transactions").aggregate(
[
{
"$group" : {
"_id" : "$orderId",
"items" : {
"$push" : "$$ROOT"
}
}
},
{
"$project" : {
"orderId" : "$_id",
"items" : "$items",
"_id" : 0
}
},
{
"$unwind" : {
"path" : "$items",
"preserveNullAndEmptyArrays" : false
}
},
{
"$group" : {
"_id" : "$items.factoryId",
"orders" : {
"$push" : "$$ROOT"
}
}
},
{
"$project" : {
"factoryId" : "$_id",
"orders" : "$orders",
"_id" : 0
}
}
]
);
I have following offers collection
Here readBy contains the _id of the users...
Now I want to count the number of unRead offers for the userId = "5add82620d7f5b38240c63d4"
{
"_id" : ObjectId("5aeaab5ed6a9c97d0209260a"),
"expiresIn" : ISODate("2018-05-30T18:30:00.000Z"),
"name" : "Trip ",
"readBy" : [
ObjectId("5add82620d7f5b38240c63d4"),
ObjectId("5add82620d7f5b38240c63c6")
],
"__v" : 0
}
{
"_id" : ObjectId("5aeaab7dd6a9c97d0209260b"),
"expiresIn" : ISODate("2018-05-29T18:30:00.000Z"),
"name" : "Trip",
"readBy" : [ObjectId("5add82620d7f5b38240c63d4")],
"__v" : 0
}
{
"_id" : ObjectId("5aeae233d6a9c97d02092622"),
"expiresIn" : ISODate("2018-05-25T18:30:00.000Z"),
"name" : "two way off",
"readBy" : [],
}
{
"_id" : ObjectId("5aeae49643f10d284726069c"),
"expiresIn" : ISODate("2018-05-25T18:30:00.000Z"),
"name" : "two way off",
"readBy" : [],
}
{
"_id" : ObjectId("5aeae49743f10d284726069d"),
"expiresIn" : ISODate("2018-05-25T18:30:00.000Z"),
"name" : "two way off",
"readBy" : []
}
{
"_id" : ObjectId("5aeae49743f10d284726069e"),
"expiresIn" : ISODate("2018-05-25T18:30:00.000Z"),
"name" : "two way off",
"readBy" : []
}
so for the above collection my output should be
[{
numberOfUnreadOffers: 4
}]
because four of the collection does not have 5add82620d7f5b38240c63d4 in readBy array
You basically use $setIsSubset and $cond here:
var userId= "5add82620d7f5b38240c63d4";
Model.aggregate([
{ "$group": {
"_id": null,
"numberOfUnreadOffers": {
"$sum": {
"$cond": {
"if": {
"$setIsSubset": [[mongoose.Types.ObjectId(userId)], "$readBy"]
},
"then": 0,
"else": 1
}
}
}
}}
])
Of course you also need to "cast" using mongoose.Types.ObjectId from the "string" to a valid ObjectId value.
But really you get better performance from a simple count() instead:
Model.count({ "readBy": { "$ne": userId } })
So you really should not use .aggregate() for this at all.
I have a schema in which it has some fields..
i am not able to find query for this, i tried $group but was not able to find results
collection: tasks
{
"_id" : ObjectId("5a475ee4b342fa03e71192bd"),
"title" : "Some Title",
"assignedUsers" : [
{
"_id" : ObjectId("5a47386ee4788102e530f60d"),
"name" : "Sam",
"status" : "Unconfirmed"
},
{
"_id" : ObjectId("5a473878e4788102e530f60f"),
"name" : "Ricky",
"status" : "Rejected"
}
{
"_id" : ObjectId("5a47388be4788102e530f611"),
"name" : "Niel",
"status" : "Unconfirmed"
},
{
"_id" : ObjectId("5a47388be4788102e530f611"),
"name" : "ABC",
"status" : "Unconfirmed"
},
{
"_id" : ObjectId("5a473892e4788102e530f612"),
"name" : "Rocky",
"status" : "Rejected"
}
]
}
Result should contain
Unconfirmed=3
Rejected=2
Thanks
Use below query,
db.coll3.aggregate([{
$unwind: '$assignedUsers'
}, {
$group: {
_id: '$assignedUsers.status',
'count': {
$sum: 1
}
}
}
])
If you want to query against a particular document make sure, you use a $match as first stage and then use the other 2 $unwind and $group.
You would get result as
{ "_id" : "Rejected", "count" : 2 }
{ "_id" : "Unconfirmed", "count" : 3 }
Hope this helps.
If I want to perform a $group and $sum on a mongodb collection from my node server (using mongoosoe), is it possible to return 0 for the non existing groups?
the collection has the following fields: ssn, name, gender, city.
model.aggregate([
{
$group : { _id : { city:"$city", gender:"$gender"}, count{ $sum:1 }}
}], function (err,result) {
if(err) {
//err
}
else{
//response
}
});
if there are people of both genders in the city - the query will return:
{
"_id" : {
"city" : "NY",
"gender" : "male"
},
"count" : 11
},
{
"_id" : {
"city" : "NY",
"gender" : "female"
},
"count" : 31
}
but if people of one gender are not present in a city - no value will be returned. for example no males in LA:
{
"_id" : {
"city" : "LA",
"gender" : "female"
},
"count" : 53
}
is it possible to make the query return the following result for given scenario without having a collection with cities and population quantities?
{
"_id" : {
"city" : "LA",
"gender" : "male"
},
"count" : 0
},
{
"_id" : {
"city" : "LA",
"gender" : "female"
},
"count" : 53
}
thanks,
If the possible values are finite and known as in your example, you could use $cond to combine the counts for male and female into one document per city like this:
[
{
$group : {
_id: {
city:"$city"
},
males:{
$sum: {
$cond: {if: {$eq:["$gender", "male"]}, then: 1, else: 0}
}
},
females:{
$sum: {
$cond: {if: {$eq:["$gender", "female"]}, then: 1, else: 0}
}
}
}
}
]