I'm working on an eCommerce based website using MongoDb.
In my db collection I have 2 type of documents
Company details Item
{ doc_type : 'company',
name : 'Acer',
type : 'Laptops',
helpline : '1800-200-000'
}
Item Details
{doc_type : "item",
item_id : 1001,
price : 2000,
discount : 20}
Now in product page i need to get data from both document.
so, first i run
db.collection.find({doc_type:'item', item_id : 1001 });
to show product data and then
db.collection.find({doc_type:'company', name: "Acer"});
to get company data.
Is their any way to reduce these 2 calls to one and get data in single result set.
like
{
company : { //company data},
item : { //item details }
}
To achieve the sample output that you have shared, along with the $match and $group stages, I have added a $project stage.
db.col.aggregate([
{
$match:
{
$or: [
{doc_type:'item', item_id : 1001 },
{doc_type:'company', name: 'Acer'}
]
}
},
{
$group:
{
_id: null,
"company_name": {$max: "$name"},
"company_type": {$max: "$type"},
"company_helpline": {$max: "$helpline"},
"item_price": {$max: "$price"},
"item_discount": {$max: "$discount"}
}
},
{
$project:
{
_id: 0,
'company' : {
'name': '$company_name',
'type': '$company_type',
'helpline': '$company_helpline',
},
'item' : {
'price': '$item_price',
'discount': '$item_discount'
}
}
}
]).pretty()
Output :
{
"company" : {
"name" : "Acer",
"type" : "Laptops",
"helpline" : "1800-200-000"
},
"item" : {
"price" : 2000,
"discount" : 20
}
}
You can achieve this using aggregation using a $match and a $group stage.
The query would be :
db.it.aggregate([
{$match:
{$or: [
{doc_type:'item', item_id : 1001 },
{doc_type:'company', name: "Acer"}
]
}
},
{$group:
{_id: null,
"compagny_name": {$max: "$name"},
"compagny_type": {$max: "$type"},
"compagny_helpline": {$max: "$helpline"},
"item_price": {$max: "$price"},
"item_discount": {$max: "$discount"}
}
}] )
this output :
{
"_id":null,
"compagny_name":"Acer",
"compagny_type":"Laptops",
"compagny_helpline":"1800-200-000",
"item_price":2000,
"item_discount":20
}
Related
Given the following Data:
> db.users.find({}, {name: 1, createdAt: 1, updatedAt: 1}).limit(5).pretty()
{
"_id" : ObjectId("5ec8f74f32973c7b7cb7cce9"),
"createdAt" : ISODate("2020-05-23T10:13:35.012Z"),
"updatedAt" : ISODate("2020-08-20T13:37:09.861Z"),
"name" : "Patrick Jere"
}
{
"_id" : ObjectId("5ec8ef8a2b6e5f78fa20443c"),
"createdAt" : ISODate("2020-05-23T09:40:26.089Z"),
"updatedAt" : ISODate("2020-07-23T07:54:01.833Z"),
"name" : "Austine Wiga"
}
{
"_id" : ObjectId("5ed5e1a3962a3960ad85a1a2"),
"createdAt" : ISODate("2020-06-02T05:20:35.090Z"),
"updatedAt" : ISODate("2020-07-29T14:02:52.295Z"),
"name" : "Biasi Phiri"
}
{
"_id" : ObjectId("5ed629ec6d87382c608645d9"),
"createdAt" : ISODate("2020-06-02T10:29:00.204Z"),
"updatedAt" : ISODate("2020-06-02T10:29:00.204Z"),
"name" : "Chisambwe Kalusa"
}
{
"_id" : ObjectId("5ed8d21f42bc8115f67465a8"),
"createdAt" : ISODate("2020-06-04T10:51:11.546Z"),
"updatedAt" : ISODate("2020-06-04T10:51:11.546Z"),
"name" : "Wakun Moyo"
}
...
Sample Data
I use the following query to return new_users by months:
db.users.aggregate([
{
$group: {
_id: {$dateToString: {format: '%Y-%m', date: '$createdAt'}},
new_users: {
$sum: {$ifNull: [1, 0]}
}
}
}
])
example result:
[
{
"_id": "2020-06",
"new_users": 125
},
{
"_id": "2020-07",
"new_users": 147
},
{
"_id": "2020-08",
"new_users": 43
},
{
"_id": "2020-05",
"new_users": 4
}
]
and this query returns new_users, active_users and total users for a specific month.
db.users.aggregate([
{
$group: {
_id: null,
new_users: {
$sum: {
$cond: [{
$gte: ['$createdAt', ISODate('2020-08-01')]
}, 1, 0]
}
},
active_users: {
$sum: {
$cond: [{
$gt: ['$updatedAt', ISODate('2020-02-01')]
}, 1, 0]
}
},
total_users: {
$sum: {$ifNull: [1, 0]}
}
}
}
])
How can I get the second query to return results by months just like in the first query?
expected results based on one month filter:
[
{ _id: '2020-09', new_users: 0, active_users: 69},
{ _id: '2020-08', new_users: 43, active_users: 219},
{ _id: '2020-07', new_users: 147, active_users: 276},
{ _id: '2020-06', new_users: 125, active_users: 129},
{ _id: '2020-05', new_users: 4, active_users: 4}
]
You can try below aggregation.
Count new users followed by look up to count the active users for the time window for each year month.
db.users.aggregate([
{"$group":{
"_id":{"$dateFromParts":{"year":{"$year":"$createdAt"},"month":{"$month":"$createdAt"}}},
"new_users":{"$sum":1}
}},
{"$lookup":{
"from":"users",
"let":{"end_date":"$_id", "start_date":{"$dateFromParts":{"year":{"$year":"$_id"},"month":{"$subtract":[{"$month":"$_id"},1]}}}},
"pipeline":[
{"$match":{"$expr":
{"$and":[{"$gte":[
"$updatedAt",
"$$start_date"
]}, {"$lt":[
"$updatedAt",
"$$end_date"
]}]}
}},
{"$count":"activeUserCount"}
],
"as":"activeUsers"
}},
{"$project":{
"year-month":{"$dateToString":{"format":"%Y-%m","date":"$_id"}},
"new_users":1,
"active_users":{"$arrayElemAt":["$activeUsers.activeUserCount", 0]},
"_id":0
}}])
You can do the same, that you did in first query, group by cteatedAt, no need to use $ifNull operator in total_users,
Playground
Updated,
use $facet group by month and count for both counts
$project to concat both arrays using $concatArrays
$unwind deconstruct array root
$group by month and merge both month and count
Playground
Hello I have a BD with many fields where an user can enter many times, I need to create a query where I can Group by user and bring me the last entry date in the system, but other additional data such as previous and the ID of the transaction, the date is createdAT, it brings me the date but not the last one ... here the code:
db.getCollection("usersos").aggregate(
[
{
"$group" : {
"_id" : {
"_id" : "$_id",
"user" : "$user",
"previo" : "$previo"
},
"MAX(createdAt)" : {
"$max" : "$createdAt"
}
}
},
{
"$project" : {
"user" : "$_id.user",
"MAX(createdAt)" : "$MAX(createdAt)",
"_id" : "$_id._id",
"previo" : "$_id.previo"
}
}
]
);
Im staring in nosql, some help thank.....and excuseme the mstake....
Grouping by $_id will mean that every input document is a separate group, i.e. no grouping will really happen.
You could try pre-sorting by createdAt, which might be helped by an index on that field, then the group can select $first to get the first entry for each field that you care about.
db.usersos.aggregate([
{$sort: {createdAt: -1}},
{$group: {
_id:"$user",
docId: {$first: "$_id"},
previo: {$first: "$previo"},
createdAt: {$first: "$createdAt"}
}},
{ $project: {
user: "$_id",
_id: "$docId",
previo: 1,
createdAt: 1
}}
])
I have a dataset in mongodb collection named visitorsSession like
{ip : 192.2.1.1,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.3.1.8,country : 'UK', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.5.1.4,country : 'UK', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.8.1.7,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.1.1.3,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'}
I am using this mongodb aggregation
[{$match: {
nsp : "/hrm.sbtjapan.com",
creationDate : {
$gte: "2019-12-15T00:00:00.359Z",
$lte: "2019-12-20T23:00:00.359Z"
},
type : "Visitors"
}}, {$group: {
_id : "$country",
totalSessions : {
$sum: 1
}
}}, {$project: {
_id : 0,
country : "$_id",
totalSessions : 1
}}, {$sort: {
country: -1
}}]
using above aggregation i am getting results like this
[{country : 'US',totalSessions : 3},{country : 'UK',totalSessions : 2}]
But i also total visitors also along with result like totalVisitors : 5
How can i do this in mongodb aggregation ?
You can use $facet aggregation stage to calculate total visitors as well as visitors by country in a single pass:
db.visitorsSession.aggregate( [
{
$match: {
nsp : "/hrm.sbtjapan.com",
creationDate : {
$gte: "2019-12-15T00:00:00.359Z",
$lte: "2019-12-20T23:00:00.359Z"
},
type : "Visitors"
}
},
{
$facet: {
totalVisitors: [
{
$count: "count"
}
],
countrySessions: [
{
$group: {
_id : "$country",
sessions : { $sum: 1 }
}
},
{
$project: {
country: "$_id",
_id: 0,
sessions: 1
}
}
],
}
},
{
$addFields: {
totalVisitors: { $arrayElemAt: [ "$totalVisitors.count" , 0 ] },
}
}
] )
The output:
{
"totalVisitors" : 5,
"countrySessions" : [
{
"sessions" : 2,
"country" : "UK"
},
{
"sessions" : 3,
"country" : "US"
}
]
}
You could be better off with two queries to do this.
To save the two db round trips following aggregation can be used which IMO is kinda verbose (and might be little expensive if documents are very large) to just count the documents.
Idea: Is to have a $group at the top to count documents and preserve the original documents using $push and $$ROOT. And then before other matches/filter ops $unwind the created array of original docs.
db.collection.aggregate([
{
$group: {
_id: null,
docsCount: {
$sum: 1
},
originals: {
$push: "$$ROOT"
}
}
},
{
$unwind: "$originals"
},
{ $match: "..." }, //and other stages on `originals` which contains the source documents
{
$group: {
_id: "$originals.country",
totalSessions: {
$sum: 1
},
totalVisitors: {
$first: "$docsCount"
}
}
}
]);
Sample O/P: Playground Link
[
{
"_id": "UK",
"totalSessions": 2,
"totalVisitors": 5
},
{
"_id": "US",
"totalSessions": 3,
"totalVisitors": 5
}
]
I'm using an example from mongo docs which I've changed a bit:
db.books.aggregate(
[
{ $group : { _id : "$genre", books: { $push: "$$ROOT" } } }
]
)
This query will return an array of books by genre.
I want to customise it a bit, so that I would not get extra data. The following example would be a dummy one, but I'm curious if it could be implemented in mongo. I want my aggregation to return an array of groups where if genre is 'Tragedy' only 1 book would be fetched and there would be a booksCount field and in all the other cases books will be an array and there won't be a booksCount.
So the aggregation result would look something like this:
[
{ _id: '_id of tragedy genre', book: {some book}, booksCount: some int },
{ _id: '_id of some other genre', books: [books] },
...
]
So I want groups to have different keys depending on some condition
One way to do this is with the $facet aggregation pipeline stage. This stage allows us to create multiple pipelines with the same input documents. In this case we have one pipeline for the tragedy genre and another for all the other genres. In order to get your desired output we need to merge the two pipeline stages. From the docs:
Each sub-pipeline has its own field in the output document where its results are stored as an array of documents.
Because the facet stage returns an array of documents for each pipeline, we need to: concatenate these arrays together, unwind the resulting array so that each element is its own document, and then replace the root of each document to get rid of the unwanted key.
Example
Say you have the following documents:
db.books.insertMany([{
genre: "Tragedy",
title: "Romeo and Juliet"
}, {
genre: "Tragedy",
title: "Titanic"
}, {
genre: "Comedy",
title: "Hitchhikers Guide to the Galaxy"
}, {
genre: "Comedy",
title: "Blazing Saddles"
}, {
genre: "Thriller",
title: "Shutter Island"
}, {
genre: "Thriller",
title: "Hannibal"
}])
Then you can use the following query:
db.books.aggregate([{
$facet: {
tragedy: [{
$match: {genre: "Tragedy"}
}, {
$group: {
_id: "$genre",
books: {$push: "$$ROOT"}
}
}, {
$project: {
book: {$arrayElemAt: ["$books", 1]},
booksCount: {$size: "$books"}
}
}],
other: [{
$match: {
genre: {$ne: "Tragedy"}
}
}, {
$group: {
_id: "$genre",
books: {$push: "$$ROOT"}
}
}]
}
}, {
$project: {
documents: {$concatArrays: ["$tragedy", "$other"]}
}
}, {
$unwind: "$documents"
}, {
$replaceRoot: {newRoot: "$documents"}
}])
To produce:
{
"_id" : "Tragedy",
"book" : {
"_id" : ObjectId("5c59f15bc59454560b36a5c7"),
"genre" : "Tragedy",
"title" : "Titanic"
},
"booksCount" : 2
}
{
"_id" : "Thriller",
"books" : [
{
"_id" : ObjectId("5c59f15bc59454560b36a5ca"),
"genre" : "Thriller",
"title" : "Shutter Island"
},
{
"_id" : ObjectId("5c59f15bc59454560b36a5cb"),
"genre" : "Thriller",
"title" : "Hannibal"
}
]
}
{
"_id" : "Comedy",
"books" : [
{
"_id" : ObjectId("5c59f15bc59454560b36a5c8"),
"genre" : "Comedy",
"title" : "Hitchhikers Guide to the Galaxy"
},
{
"_id" : ObjectId("5c59f15bc59454560b36a5c9"),
"genre" : "Comedy",
"title" : "Blazing Saddles"
}
]
}
My Document Structure(Only 2 given just for the idea):
/* 1 */
{
"_id" : ObjectId("59edc58af33e9b5988b875fa"),
"Agent" : {
"Name" : "NomanAgent",
"Location" : "Lahore",
"AgentId" : 66,
"Suggestion" : [
"Knowledge",
"Professionalisn"
]
},
"Rating" : 2,
"Status" : "Submitted"
}
/* 2 */
{
"_id" : ObjectId("59edc58af33e9b5988b875fb"),
"Agent" : {
"Name" : "NomanAgent",
"Location" : "Lahore",
"AgentId" : 66,
"Suggestion" : [
"Knowledge",
"Clarity"
]
},
"Rating" : 1,
"Status" : "Submitted"
}
/* 3 */
{
"_id" : ObjectId("59edc58af33e9b5988b875fc"),
"Agent" : {
"Name" : "NomanAgent",
"Location" : "Lahore",
"AgentId" : 66,
"Reward" : "Thumb Up"
},
"Rating" : 5,
"Status" : "Submitted"
}
These are basically the survey responses, so an Agent object could contain either a Suggestion(in case of bad customer review) or a Reward(in case of a happy customer) so here I am showing 2 documents with Suggestions and 1 with Reward.
I have created a query for the Rewards which is given below,
db.getCollection('_survey.response').aggregate([
{
$group:{
_id: "$Agent.Name",
Rating: {$avg: "$Rating"},
Rewards: {$push: "$Agent.Reward"},
Status: {$push : "$Status"}
}
},
{
$unwind: "$Rewards"
},
{
$group:{
_id: {
Agent: "$_id",
Rating: "$Rating",
Rewards: "$Rewards"
},
RewardCount:{$sum: 1},
SurveyStatus: {$first: "$Status"}
}
},
{
$group:{
_id: "$_id.Agent",
Rewards: {$push:{Reward: "$_id.Rewards", Count: "$RewardCount"}},
Rating: {$first: "$_id.Rating"},
SurveyStatus: {$first: "$SurveyStatus"}
}
},
{
$unwind: "$SurveyStatus"
},
{
$group:{
_id: {
Agent: "$_id",
Survey: "$SurveyStatus"
},
StatusCount:{$sum : 1},
Rating: {$first: "$Rating"},
Rewards: {$first: "$Rewards"}
}
},
{
$group:{
_id: "$_id.Agent",
Status:{$push:{Status: "$_id.Survey", Count: "$StatusCount"}},
Rewards: {$first: "$Rewards"},
Rating: {$first: "$Rating"}
}
},
{
$project:{
_id: 0,
Agent: "$_id",
Rating: {
$multiply:[
{$divide:["$Rating",5]},
100
]
},
Status: 1,
Rewards: 1
}
}
]);
Above query works perfectly fine for the rewards, i want exactly the same thing for suggestions and i would be happy if its possible to adjust Suggestions in the same query(We can also create a separate query for suggestion).
Response of above given query:
/* 1 */
{
"Status" : [
{
"Status" : "Submitted",
"Count" : 2.0
},
{
"Status" : "Pending",
"Count" : 1.0
},
{
"Status" : "Opened",
"Count" : 2.0
}
],
"Rewards" : [
{
"Reward" : "Thumb Up",
"Count" : 1.0
},
{
"Reward" : "Thank You",
"Count" : 2.0
}
],
"Agent" : "GhazanferAgent",
"Rating" : 68.0
}
/* 2 */
{
"Status" : [
{
"Status" : "Opened",
"Count" : 2.0
},
{
"Status" : "Viewed",
"Count" : 2.0
},
{
"Status" : "Pending",
"Count" : 3.0
}
],
"Rewards" : [
{
"Reward" : "Gift",
"Count" : 1.0
},
{
"Reward" : "Thumb Up",
"Count" : 3.0
},
{
"Reward" : "Thank You",
"Count" : 1.0
}
],
"Agent" : "NomanAgent",
"Rating" : 60.0
}
What I have tried so far, I think of two approaches but find an issue with each of them,
First(Find avg rating and push status and suggestions in array):
db.getCollection("_survey.response").aggregate([
{
$match:
{
$and:[
{
"Agent.Suggestion":{
$exists: true
}
},
{
Rating: {$lte: 3}
}
]
}
},
{
$group:{
_id: {
AgentName: "$Agent.Name",
AgentId: "$Agent.AgentId",
Location: "$Agent.Location"
},
Rating: {$avg: "$Rating"},
Status: {$push : "$Status"},
Suggestions: {$push: "$Agent.Suggestion"}
}
}
]);
Issue facing with this approach is, suggestions in the projection will become an array of arrays(as it was initially an array) of dynamic size depending on the number of times an agent gets a suggestion in a customer response. So the problem is applying $unwind on 2D array of dynamic size.
Second($unwind the suggestions in the first stage as its a 1D array
to avoid $unwind issue on 2D array of dynamic size)
db.getCollection("_survey.response").aggregate([
{
$match:
{
$and:[
{
"Agent.Suggestion":{
$exists: true
}
},
{
Rating: {$lte: 3}
}
]
}
},
{
$unwind: "$Agent.Suggestion"
},
{
$group: {
_id:{
AgentName: "$Agent.Name",
AgentId: "$Agent.AgentId",
Suggestion: "$Agent.Suggestion",
Location: "$Agent.Location"
},
Status: {$push: "$Status"},
Rating: {$avg: "$Rating"},
Count: {$sum: 1}
}
}
]);
Problem using this approach is $unwind Suggestion array it will flatten all suggestion with their respective agents thus increasing the number of documents(as compared to original responses) so i won't be able to find correct value for average rating for each agent on the basis of this grouping and the same will happen the Status(Because i can correctly find these two fields only if i group by agent. While, here i am grouping with agent along with suggestion),
I want exactly the same response for Suggestion query, only the Rewards object in response would replace Suggestions(Or it would great if we could get Suggestions object in the same response)
Survey Status can be, pending, Opened,viewed, Submitted etc
Output explanation:
I want suggestions(with counts), status(with counts) and Rating in % form(which i am already doing) for each of the agent as you can see in the output mentioned above.
Thanks in advance!!
Using $unwind two consecutive times did the trick for me, using First approach,
db.getCollection("_survey.response").aggregate([
{
$match:
{
$and:[
{
"Agent.Suggestion":{
$exists: true
}
},
{
Rating: {$lte: 3}
}
]
}
},
{
$group:{
_id: {
AgentName: "$Agent.Name",
AgentId: "$Agent.AgentId",
Location: "$Agent.Location"
},
Rating: {$avg: "$Rating"},
Status: {$push : "$Status"},
Suggestions: {$push: "$Agent.Suggestion"}
}
},
{
$unwind: "$Suggestions"
},
{
$unwind: "$Suggestions"
},
{
$group: {
_id: {
Suggestions: "$Suggestions",
AgentName: "$_id.AgentName",
AgentId: "$_id.AgentId",
Location: "$_id.Location"
},
SuggestionCount: {$sum: 1},
Rating: {$first: "$Rating"},
Status: {$first: "$Status"}
}
},
{
$group: {
_id:{
AgentName: "$_id.AgentName",
AgentId: "$_id.AgentId",
Location: "$_id.Location"
},
Suggestions: {$push:{Sugestion: "$_id.Suggestions", Count: "$SuggestionCount"}},
TotalSuggestions: {$sum: "$SuggestionCount"},
Rating: {$first: "$Rating"},
Status: {$first: "$Status"}
}
},
{
$unwind: "$Status"
},
{
$group:{
_id: {
AgentName: "$_id.AgentName",
AgentId: "$_id.AgentId",
Location: "$_id.Location",
Status: "$Status"
},
StatusCount:{$sum : 1},
Rating: {$first: "$Rating"},
Suggestions: {$first: "$Suggestions"},
TotalSuggestions: {$first: "$TotalSuggestions"}
}
},
{
$group:{
_id: {
AgentName: "$_id.AgentName",
AgentId: "$_id.AgentId",
Location: "$_id.Location"
},
Status:{$push:{Status: "$_id.Status", Count: "$StatusCount"}},
TotalStatus: {$sum: "$StatusCount"},
Suggestions: {$first: "$Suggestions"},
TotalSuggestions: {$first: "$TotalSuggestions"},
Rating: {$first: "$Rating"}
}
},
{
$project: {
_id: 0,
AgentName: "$_id.AgentName",
AgentId: "$_id.AgentId",
Location: "$_id.Location",
Status: 1,
TotalStatus: 1,
Suggestions: 1,
TotalSuggestions: 1,
Performance: {
$concat: [
{
$substr: [
{
$multiply:[
{$divide:["$Rating",5]},
100
]
}, 0, 4
]
},"%"
]
}
}
}
]);