MongoDB aggregation and projection - mongodb

Can someone help me with the query for sorting an array by date in ascending order and as well display the cCode? I am able to sort the array and project it but am unable to project the cCode along with bal array,
db.collection.aggregate([
{ "$match": {
"_id": {
"$eq": {
"a": "NA",
"b": "HXYZ",
"c": "12345",
"d": "AA"
}
}
}},
{ "$unwind": "$bal" },
{ "$sort": { "bal.date": 1 }},
{ "$group": {"_id": "$_id",
"bal": {"$push": "$bal"}}},
{ $project: {
bal: { $slice: ["$bal",2]} ,"cCode": 1}
}
])
My collection:
/* 1 */
{
"_id" : {
"a" : "NA",
"b" : "HXYZ",
"c" : "12345",
"d" : "AA"
},
"cCode" : "HHH",
"bal" : [
{
"type" : "E",
"date" : "2015-08-02"
},
{
"type" : "E",
"date" : "2015-08-01"
},
{
"type" : "E",
"date" : "2015-07-07"
}
]
}
Please help me what is the problem in the above query. Thanks in advance.

Your cCode field vanished when you use $group stage. So, To get that field again in the pipeline you need to use $first aggregation. Something like this
db.collection.aggregate([
{ "$match": {
"_id": { "$eq": { "a": "NA", "b": "HXYZ", "c": "12345", "d": "AA" }}
}},
{ "$unwind": "$bal" },
{ "$sort": { "bal.date": 1 }},
{ "$group": {
"_id": "$_id",
"bal": { "$push": "$bal" },
"cCode": { "$first": "$cCode" }
}},
{ "$project": { "bal": { "$slice": ["$bal", 2] } ,"cCode": 1 }}
])

Related

MongoDB sorting by date as type String

Can someone help me with the query for sorting an array by date in ascending order?
I have tried the below query but the sorting is not happening as expected,
db.getCollection(xyz).aggregate([{
$match: {
"_id":{$in:[{"a" : "NA","b" : "HXYZ","c" : "12345","d" : "AA"}]}
}
},{
$sort: {'bal.date': 1}
},
{ $project: {
balances: { $slice: ["$bal",2]}
}
}
])
My collection:
/* 1 */
{
"_id" : {
"a" : "NA",
"b" : "HXYZ",
"c" : "12345",
"d" : "AA"
},
"bal" : [
{
"type" : "E",
"date" : "2015-08-02"
},
{
"type" : "E",
"date" : "2015-08-01"
},
{
"type" : "E",
"date" : "2015-07-07"
}
]
}
Please help me what is the problem in the above query.
Thanks in advance.
You are mixing $match with $sort stage
Correct syntax to used aggregation pipeline stages
db.collection.aggregate([
{ "$match": {
"_id": {
"$eq": {
"a": "NA",
"b": "HXYZ",
"c": "12345",
"d": "AA"
}
}
}},
{ "$unwind": "$bal" },
{ "$sort": { "bal.date": 1 }},
{ "$group": {
"_id": "$_id",
"bal": {
"$push": "$bal"
}
}}
])
From the looks of it, you're saving the date as String, the sort() function will sort the dates as Strings which will not give you the order you're expecting.
You can either run a script that will convert the bal.date field to Date() format and then sort() will work automatically, or you can do the converting + sorting server side.

mongo aggregation framework group by quarter/half year/year

I have a database with this schema structure :
{
"name" : "Carl",
"city" : "paris",
"time" : "1-2018",
"notes" : [
"A",
"A",
"B",
"C",
"D"
]
}
And this query using the aggregation framework :
db.getCollection('collection').aggregate(
[{
"$match": {
"$and": [{
"$or": [ {
"time": "1-2018"
}, {
"time": "2-2018"
} ]
}, {
"name": "Carl"
}, {
"city": "paris"
}]
}
}, {
"$unwind": "$notes"
}, {
"$group": {
"_id": {
"notes": "$notes",
"time": "$time"
},
"count": {
"$sum": 1
}
}
}
, {
"$group": {
"_id": "$_id.time",
"count": {
"$sum": 1
}
}
}, {
"$project": {
"_id": 0,
"time": "$_id",
"count": 1
}
}])
It working correcly and i'm getting these results these results :
{
"count" : 4.0,
"time" : "2-2018"
}
{
"count" : 4.0,
"time" : "1-2018"
}
My issue is that i'd like to keep the same match stage and i'd like to group by quarter.
Here the result i'd like to have :
{
"count" : 8.0,
"time" : "1-2018" // here quarter 1
}
Thanks

Aggregate query with document containing array of objects

I am facing issue with the aggregation query on MongoDB.
I have a document in following structure:
[{
"_id": ObjectId("19a5070b808028108101"),
"arr_vs": [
{
"arr_id": "one",
"val": 5
},
{
"arr_id": "two",
"val": 5
}]
},
{
"_id": ObjectId("19a5070b80802810810"),
"arr_vs": [
{
"arr_id": "one",
"val": 5
},
{
"arr_id": "two",
"val": 2
},{
"arr_id": "three",
"val": 1
}]
}]
I want the count for each value associated with arr_vs items.
Expected output:
{
"arr_vs":{
"one":[
{
"val":5,
"total_count":2
},{
"val":2,
"total_count":
}
}],
"two":[
{
"val":5,
"total_count":2
},{
"val":2,
"total_count":
}
}]
}
}
Any help will be appreciated.
Outputting to named keys is never really the fantastic thing some people seem to think it is. Realistically I usually want to work with the results returned, and therefore a "list/array" makes a lot more sense.
This is basically every new person basically gets told to abandon their "named keys" concepts, and realize they are working with a database and the inherent problems with named keys. Kind of also why collections are essentially "lists" as well.
So you would be better off getting used to the concept:
db.collection.aggregate([
{ "$unwind": "$arr_vs" },
{ "$group": {
"_id": { "id": "$arr_vs.arr_id", "val": "$arr_vs.val" },
"total_count": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.id",
"v": {
"$push": {
"val": "$_id.val",
"total_count": "$total_count"
}
}
}}
])
Which is basically going to give you:
/* 1 */
{
"_id" : "two",
"v" : [
{
"val" : 2.0,
"total_count" : 1.0
},
{
"val" : 5.0,
"total_count" : 1.0
}
]
}
/* 2 */
{
"_id" : "one",
"v" : [
{
"val" : 5.0,
"total_count" : 2.0
}
]
}
/* 3 */
{
"_id" : "three",
"v" : [
{
"val" : 1.0,
"total_count" : 1.0
}
]
}
And is the aggregated data in an iterable and easy to use form.
If you are intent on your output format and have at least a MongoDB 3.4.4 version, you can take that further by compacting the documents and using $arrayToObject:
db.collection.aggregate([
{ "$unwind": "$arr_vs" },
{ "$group": {
"_id": { "id": "$arr_vs.arr_id", "val": "$arr_vs.val" },
"total_count": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.id",
"v": {
"$push": {
"val": "$_id.val",
"total_count": "$total_count"
}
}
}},
{ "$group": {
"_id": null,
"arr_vs": {
"$push": {
"k": "$_id",
"v": "$v"
}
}
}},
{ "$project": {
"_id": 0,
"arr_vs": { "$arrayToObject": "$arr_vs" }
}}
])
Or even just apply the final "reshape" client side, if your MongoDB version does not support the new operator:
db.collection.aggregate([
{ "$unwind": "$arr_vs" },
{ "$group": {
"_id": { "id": "$arr_vs.arr_id", "val": "$arr_vs.val" },
"total_count": { "$sum": 1 }
}},
{ "$group": {
"_id": "$_id.id",
"v": {
"$push": {
"val": "$_id.val",
"total_count": "$total_count"
}
}
}},
{ "$group": {
"_id": null,
"arr_vs": {
"$push": {
"k": "$_id",
"v": "$v"
}
}
}},
/*
{ "$project": {
"_id": 0,
"arr_vs": { "$arrayToObject": "$arr_vs" }
}}
*/
]).map( d => ({
"arr_vs": d.arr_vs.reduce((acc,curr) =>
Object.assign(acc,({ [curr.k]: curr.v })),{})
}))
And both produce the same output:
{
"arr_vs" : {
"two" : [
{
"val" : 2.0,
"total_count" : 1.0
},
{
"val" : 5.0,
"total_count" : 1.0
}
],
"one" : [
{
"val" : 5.0,
"total_count" : 2.0
}
],
"three" : [
{
"val" : 1.0,
"total_count" : 1.0
}
]
}
}

Mongodb group by and sum and get media with Map

I have these collections in my database:
Items:
{ "IdUser" : "1", "IdItem" : "1" },
{ "IdUser" : "1", "IdItem" : "2" },
{ "IdUser" : "1", "IdItem" : "3" },
{ "IdUser" : "2", "IdItem" : "4" },
{ "IdUser" : "2", "IdItem" : "5" },
{ "IdUser" : "4", "IdItem" : "6" },
{ "IdUser" : "5", "IdItem" : "7" }
Users
{ "_id" : "1", "DateRegister" : ISODate("2016-03-29T22:00:38.764+0000") },
{ "_id" : "2", "DateRegister" : ISODate("2014-03-29T22:00:38.764+0000") },
{ "_id" : "2", "DateRegister" : ISODate("2015-02-29T22:00:38.764+0000") },
{ "_id" : "4", "DateRegister" : ISODate("2013-01-29T22:00:38.764+0000") },
{ "_id" : "5", "DateRegister" : ISODate("2016-04-29T22:00:38.764+0000") }
How can I obtain this result but FILTERED with users registered after 2015:
Users with one item: 2
Users with two items: 1
Users with three items: 1
I have tried with that, but I don't know how to filter... Thanks!
db.collection.aggregate([
{
"$group": {
"_id": "$IdUser",
"count": {
"$sum": { "$cond": [{ "$gt": [ "$IdItem", null ] }, 1, 0 ] }
}
}
},
{
"$group": {
"_id": "$count",
"users": { "$push": "$_id" }
}
},
{
"$project": {
"_id": 0,
"number_of_items": "$_id",
"number_of_users": { "$size": "$users" }
}
}
])
You may want to utilize the $lookup operator to perform a join of the items collection with the users collection and then do a $match filter on the DateRegistered field before piping the main grouping operations.
Following this example + the links herein to the documentation will give you an idea:
db.items.aggregate([
{
"$lookup": {
"from": "users",
"localField": "IdUser",
"foreignField": "_id",
"as": "user"
}
},
{ "$match": { "user.DateRegister": { "$gt": new Date(2015, 11, 31) } } },
{
"$group": {
"_id": "$IdUser",
"count": {
"$sum": { "$cond": [{ "$gt": [ "$IdItem", null ] }, 1, 0 ] }
}
}
},
{
"$group": {
"_id": "$count",
"users": { "$push": "$_id" }
}
},
{
"$project": {
"_id": 0,
"number_of_items": "$_id",
"number_of_users": { "$size": "$users" }
}
}
])
In the event that your MongoDB server does not support the $lookup operator, you will then need a workaround where you split the operations on the different collections i.e.
get a list of user id's that match the given date range criteria, this could be done with the distinct() method on the users collection with the date query option.
use that list in the items collection aggregation pipeline within the $match operator initial step.
The following demonstrates this:
// use distinct to get the user id's list
var userIds = db.users.distinct("_id", { "DateRegister": { "$gt": new Date(2015, 11, 31) } })
// perform your aggregation with a filtered collection using the list from the above operations
db.items.aggregate([
{ "$match": { "IdUser": { "$in": userIds } } },
{
"$group": {
"_id": "$IdUser",
"count": {
"$sum": { "$cond": [{ "$gt": [ "$IdItem", null ] }, 1, 0 ] }
}
}
},
{
"$group": {
"_id": "$count",
"users": { "$push": "$_id" }
}
},
{
"$project": {
"_id": 0,
"number_of_items": "$_id",
"number_of_users": { "$size": "$users" }
}
}
])

MongoDB aggregate/grouping by key-value pairs

My data looks something like this:
{
"_id" : "9aa072e4-b706-47e6-9607-1a39e904a05a",
"customerId" : "2164289-4",
"channelStatuses" : {
"FOO" : {
"status" : "done"
},
"BAR" : {
"status" : "error"
}
},
"channel" : "BAR",
}
My aggregate/group looks like this:
{
"_id" : {
"customerId" : "$customerId",
"channel" : "$channel",
"status" : "$channelStatuses[$channel].status"
},
"count" : {
"$sum" : 1
}
}
So basically with the example data the group should give me a group grouped by:
{"customerId": "2164289-4", "channel": "BAR", "status": "error"}
But I cannot use []-indexing in a aggregate/group. What should I do instead?
You cannot get the result you want with the current structure using .aggregate(). You "could" change the structure to use an array rather than named keys, and the operation is actually quite simple.
So with a document like:
{
"_id" : "9aa072e4-b706-47e6-9607-1a39e904a05a",
"customerId" : "2164289-4",
"channelStatuses" : [
{
"channel": "FOO",
"status" : "done"
},
{
"channel": "BAR",
"status" : "error"
}
],
"channel" : "BAR",
}
You can then do in modern releases with $filter, $map and $arrayElemAt:
{ "$group": {
"_id": {
"customerId" : "$customerId",
"channel" : "$channel",
"status": {
"$arrayElemAt": [
{ "$map": {
"input": { "$filter": {
"input": "$chanelStatuses",
"as": "el",
"cond": { "$eq": [ "$$el.channel", "$channel" ] }
}},
"as": "el",
"in": "$$el.status"
}},
0
]
}
},
"count": { "$sum": 1 }
}}
Older versions of MongoDB are going to going to require $unwind to access the matched array element.
In MongoDB 2.6 then you can still "pre-filter" the array before unwind:
[
{ "$project": {
"customerId": 1,
"channel": 1,
"status": {
"$setDifference": [
{ "$map": {
"input": "$channelStatuses",
"as": "el",
"in": {
"$cond": [
{ "$eq": [ "$$el.channel", "$channel" ] },
"$$el.status",
false
]
}
}},
[false]
]
}
}},
{ "$unwind": "$status" },
{ "$group": {
"_id": {
"customerId": "$customerId",
"channel": "$channel",
"status": "$status"
},
"count": { "$sum": 1 }
}}
]
And anything prior to that you "filter" after $unwind instead:
[
{ "$unwind": "$channelStatuses" },
{ "$project": {
"customerId": 1,
"channel": 1,
"status": "$channelStatuses.status",
"same": { "$eq": [ "$channelStatuses.status", "$channel" ] }
}},
{ "$match": { "same": true } },
{ "$group": {
"_id": "$_id",
"customerId": { "$first": "$customerId" },
"channel": { "$first": "$channel" },
"status": { "$first": "$status" }
}},
{ "$group": {
"_id": {
"customerId": "$customerId",
"channel": "$channel",
"status": "$status"
},
"count": { "$sum": 1 }
}}
]
In a lesser version than MongoDB 2.6 you also need to $project the result of the equality test between the two fields and then $match on the result in a seperate stage. You might also note the "two" $group stages, since the first one removes any possible duplicates of the "channel" values after the filter via the $first accumulators. The following $group is exactly the same as in the previous listing.
But if you cannot change the structure and need "flexible" matching of keys where you cannot supply every name, then you must use mapReduce:
db.collection.mapReduce(
function() {
emit({
"customerId": this.customerId,
"channel": this.channel,
"status": this.channelStatuses[this.channel].status
},1);
},
function(key,values) {
return Array.sum(values);
},
{ "out": { "inline": 1 } }
)
Where of course you can use that sort of notation