Selecting Distinct values from Array in MongoDB - mongodb

I have a collection name Alpha_Num, It has following structure. I am trying to find out which Alphabet-Numerals pair will appear maximum number of times ?
If we just go with the data below, pair abcd-123 appears twice so as pair efgh-10001, but the second one is not a valid case for me as it appears in same document.
{
"_id" : 12345,
"Alphabet" : "abcd",
"Numerals" : [
"123",
"456",
"2345"
]
}
{
"_id" : 123456,
"Alphabet" : "efgh",
"Numerals" : [
"10001",
"10001",
"1002"
]
}
{
"_id" : 123456567,
"Alphabet" : "abcd",
"Numerals" : [
"123"
]
}
I tried to use aggregation frame work, something like below
db.Alpha_Num.aggregate([
{"$unwind":"$Numerals"},
{"$group":
{"_id":{"Alpha":"$Alphabet","Num":"$Numerals"},
"count":{$sum:1}}
},
{"$sort":{"count":-1}}
])
Problem in this query is it gives pair efgh-10001 twice.
Question : How to select distinct values from array "Numerals" in the above condition ?

Problem solved.
db.Alpha_Num.aggregate([{
"$unwind": "$Numerals"
}, {
"$group": {
_id: {
"_id": "$_id",
"Alpha": "$Alphabet"
},
Num: {
$addToSet: "$Numerals"
}
}
}, {
"$unwind": "$Num"
}, {
"$group": {
_id: {
"Alplha": "$_id.Alpha",
"Num": "$Num"
},
count: {
"$sum": 1
}
}
}])
Grouping using $addToSet and unwinding again did the trick. Got the answer from one of 10gen online course.

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.

Project an array with MongoDB

I'm using MongoDB's aggregation pipeline, to get my documents in the form that I want. As the last step of aggregation, I use $project to put the documents into their final form.
But I'm having trouble projecting and array of sub-documents. Here is what I currently get from aggrgation:
{
"_id": "581c8c3df1325f68ffd23386",
"count": 14,
"authors": [
{
"author": {
"author": "57f246b9e01e6c6f08e1d99a",
"post": "581c8c3df1325f68ffd23386"
},
"count": 13
},
{
"author": {
"author": "5824382511f16d0f3fd5aaf2",
"post": "581c8c3df1325f68ffd23386"
},
"count": 1
}
]
}
I want to $project the authors array so that the return would be this:
{
"_id": "581c8c3df1325f68ffd23386",
"count": 14,
"authors": [
{
"_id": "57f246b9e01e6c6f08e1d99a",
"count": 13
},
{
"_id": "5824382511f16d0f3fd5aaf2",
"count": 1
}
]
}
How would I go about achieving that?
You can unwind the array and wind it u again after projecting.
Something like this:
db.collectionName.aggregate([
{$unwind:'$authors'},
{$project:{_id:1,count:1,'author.id':'$authors.author.author','author.count':'$authors.count'}},
{$group:{_id:{_id:'$_id',count:'$count'},author:{$push:{id:'$author.id',count:'$author.count'}}}},
{$project:{_id:0,_id:'$_id._id',count:'$_id.count',author:1}}
])
the output for above will be:
{
"_id" : "581c8c3df1325f68ffd23386",
"author" : [
{
"id" : "57f246b9e01e6c6f08e1d99a",
"count" : 13.0
},
{
"id" : "5824382511f16d0f3fd5aaf2",
"count" : 1.0
}
],
"count" : 14.0
}
I have been having the same problem and just now found a simple and elegant solution that has not been mentioned anywhere, so i thought I'd share it here:
You can iterate the array using $map and project each author. With the given structure, the aggregation should look somewhat like this
db.collectionName.aggregate([
$project: {
_id: 1,
count:1,
authors: {
$map: {
input: "$authors",
as: "author",
in: {
id: "$$author.author.author",
count: $$author.author.count
}
}
}
}
])
Hope this helps anyone who is looking, like me :)
Question:
"customFields" : [
{
"index" : "1",
"value" : "true",
"label" : "isOffline",
"dataType" : "check_box",
"placeholder" : "cf_isoffline",
"valueFormatted" : "true"
},
{
"index" : "2",
"value" : "false",
"label" : "tenure_extended",
"dataType" : "check_box",
"placeholder" : "cf_tenure_extended",
"valueFormatted" : "false"
}
],
Answer:
db.subscription.aggregate([
{$match:{"autoCollect" : false,"remainingBillingCycles" : -1,"customFields.value":"false", "customFields.label" : "isOffline"}},
{$project: {first: { $arrayElemAt: [ "$customFields", 1 ] }}}
])

How to group by different fields

I want to find all users named 'Hans' and aggregate their 'age' and number of 'childs' by grouping them.
Assuming I have following in my database 'users'.
{
"_id" : "01",
"user" : "Hans",
"age" : "50"
"childs" : "2"
}
{
"_id" : "02",
"user" : "Hans",
"age" : "40"
"childs" : "2"
}
{
"_id" : "03",
"user" : "Fritz",
"age" : "40"
"childs" : "2"
}
{
"_id" : "04",
"user" : "Hans",
"age" : "40"
"childs" : "1"
}
The result should be something like this:
"result" :
[
{
"age" :
[
{
"value" : "50",
"count" : "1"
},
{
"value" : "40",
"count" : "2"
}
]
},
{
"childs" :
[
{
"value" : "2",
"count" : "2"
},
{
"value" : "1",
"count" : "1"
}
]
}
]
How can I achieve this?
This should almost be a MongoDB FAQ, mostly because it is a real example concept of how you should be altering your thinking from SQL processing and embracing what engines like MongoDB do.
The basic principle here is "MongoDB does not do joins". Any way of "envisioning" how you would construct SQL to do this essentially requires a "join" operation. The typical form is "UNION" which is in fact a "join".
So how to do it under a different paradigm? Well first, let's approach how not to do it and understand the reasons why. Even if of course it will work for your very small sample:
The Hard Way
db.docs.aggregate([
{ "$group": {
"_id": null,
"age": { "$push": "$age" },
"childs": { "$push": "$childs" }
}},
{ "$unwind": "$age" },
{ "$group": {
"_id": "$age",
"count": { "$sum": 1 },
"childs": { "$first": "$childs" }
}},
{ "$sort": { "_id": -1 } },
{ "$group": {
"_id": null,
"age": { "$push": {
"value": "$_id",
"count": "$count"
}},
"childs": { "$first": "$childs" }
}},
{ "$unwind": "$childs" },
{ "$group": {
"_id": "$childs",
"count": { "$sum": 1 },
"age": { "$first": "$age" }
}},
{ "$sort": { "_id": -1 } },
{ "$group": {
"_id": null,
"age": { "$first": "$age" },
"childs": { "$push": {
"value": "$_id",
"count": "$count"
}}
}}
])
That will give you a result like this:
{
"_id" : null,
"age" : [
{
"value" : "50",
"count" : 1
},
{
"value" : "40",
"count" : 3
}
],
"childs" : [
{
"value" : "2",
"count" : 3
},
{
"value" : "1",
"count" : 1
}
]
}
So why is this bad? The main problem should be apparent in the very first pipeline stage:
{ "$group": {
"_id": null,
"age": { "$push": "$age" },
"childs": { "$push": "$childs" }
}},
What we asked to do here is group up everything in the collection for the values we want and $push those results into an array. When things are small then this works, but real world collections would result in this "single document" in the pipeline that exceeds the 16MB BSON limit that is allowed. That is what is bad.
The rest of the logic follows the natural course by working with each array. But of course real world scenarios would almost always make this untenable.
You could avoid this somewhat, by doing things like "duplicating" the documents to be of "type" "age or "child" and grouping documents individually by type. But it's all a bit to "over complex" and not a solid way of doing things.
The natural response is "what about a UNION?", but since MongoDB does not do the "join" then how to approach that?
A Better Way ( aka A New Hope )
Your best approach here both architecturally and performance wise is to simply submit "both" queries ( yes two ) in "parallel" to the server via your client API. As the results are received you then "combine" them into a single response you can then send back as a source of data to your eventual "client" application.
Different languages have different approaches to this, but the general case is to look for an "asynchronous processing" API that allows you to do this in tandem.
My example purpose here uses node.js as the "asynchronous" side is basically "built in" and reasonably intuitive to follow. The "combination" side of things can be any type of "hash/map/dict" table implementation, just doing it the simple way for example only:
var async = require('async'),
MongoClient = require('mongodb');
MongoClient.connect('mongodb://localhost/test',function(err,db) {
var collection = db.collection('docs');
async.parallel(
[
function(callback) {
collection.aggregate(
[
{ "$group": {
"_id": "$age",
"type": { "$first": { "$literal": "age" } },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": -1 } }
],
callback
);
},
function(callback) {
collection.aggregate(
[
{ "$group": {
"_id": "$childs",
"type": { "$first": { "$literal": "childs" } },
"count": { "$sum": 1 }
}},
{ "$sort": { "_id": -1 } }
],
callback
);
}
],
function(err,results) {
if (err) throw err;
var response = {};
results.forEach(function(res) {
res.forEach(function(doc) {
if ( !response.hasOwnProperty(doc.type) )
response[doc.type] = [];
response[doc.type].push({
"value": doc._id,
"count": doc.count
});
});
});
console.log( JSON.stringify( response, null, 2 ) );
}
);
});
Which gives the cute result:
{
"age": [
{
"value": "50",
"count": 1
},
{
"value": "40",
"count": 3
}
],
"childs": [
{
"value": "2",
"count": 3
},
{
"value": "1",
"count": 1
}
]
}
So the key thing to note here is that the "separate" aggregation statements themselves are actually quite simple. The only thing you face is combining those in your final result. There are many approaches to "combining", particularly to deal with large results from each of the queries, but this is the basic example of the execution model.
Key points here.
Shuffling data in the aggregation pipeline is possible but not performant for large data sets.
Use a language implementation and API that support "parallel" and "asynchronous" execution so you can "load up" all or "most" of your operations at once.
The API should support some method of "combination" or otherwise allow a separate "stream" write to process each result set received into one.
Forget about the SQL way. The NoSQL way delegates the processing of such things as "joins" to your "data logic layer", which is what contains the code as shown here. It does it this way because it is scalable to very large datasets. It is rather the job of your "data logic" handling nodes in large applications to deliver this to the end API.
This is fast compared to any other form of "wrangling" I could possibly describe. Part of "NoSQL" thinking is to "Unlearn what you have learned" and look at things a different way. And if that way doesn't perform better, then stick with the SQL approach for storage and query.
That's why alternatives exist.
That was a tough one!
First, the bare solution:
db.test.aggregate([
{ "$match": { "user": "Hans" } },
// duplicate each document: one for "age", the other for "childs"
{ $project: { age: "$age", childs: "$childs",
data: {$literal: ["age", "childs"]}}},
{ $unwind: "$data" },
// pivot data to something like { data: "age", value: "40" }
{ $project: { data: "$data",
value: {$cond: [{$eq: ["$data", "age"]},
"$age",
"$childs"]} }},
// Group by data type, and count
{ $group: { _id: {data: "$data", value: "$value" },
count: { $sum: 1 },
value: {$first: "$value"} }},
// aggregate values in an array for each independant (type,value) pair
{ $group: { _id: "$_id.data", values: { $push: { count: "$count", value: "$value" }} }} ,
// project value to the correctly name field
{ $project: { result: {$cond: [{$eq: ["$_id", "age"]},
{age: "$values" },
{childs: "$values"}]} }},
// group all data in the result array, and remove unneeded `_id` field
{ $group: { _id: null, result: { $push: "$result" }}},
{ $project: { _id: 0, result: 1}}
])
Producing:
{
"result" : [
{
"age" : [
{
"count" : 3,
"value" : "40"
},
{
"count" : 1,
"value" : "50"
}
]
},
{
"childs" : [
{
"count" : 1,
"value" : "1"
},
{
"count" : 3,
"value" : "2"
}
]
}
]
}
And now, for some explanations:
One of the major issues here is that each incoming document has to be part of two different sums. I solved that by adding a literal array ["age", "childs"] to your documents, and then unwinding them by that array. That way, each document will be presented twice in the later stage.
Once that done, to ease processing, I change the data representation to something much more manageable like { data: "age", value: "40" }
The following steps will perform the data aggregation per-se. Up to the third $project step that will map the value fields to the corresponding age or childs field.
The final two steps will simply wrap the two documents in one, removing the unneeded _id field.
Pfff!

Check if embed exists - aggregation framework mongodb

This is my test collection:
>db.test.find()
{
"_id": ObjectId("54906479e89cdf95f5fb2351"),
"reports": [
{
"desc": "xxx",
"order": {"$id": ObjectId("53fbede62827b89e4f86c12e")}
}
]
},
{
"_id": ObjectId("54906515e89cdf95f5fb2352"),
"reports": [
{
"desc": "xxx"
}
]
},
{
"_id": ObjectId("549067d3e89cdf95f5fb2353"),
"reports": [
{
"desc": "xxx"
}
]
}
I want to count all documents and documents with order, so:
>db.test.aggregate({
$group: {
_id: null,
all: {
$sum: 1
},
order: {
$sum: {
"$cond": [
{
"$ifNull": ["$reports.order", false]
},
1,
0
]
}
}
}
})
and my results:
{
"result" : [
{
"_id" : null,
"all" : 3,
"order" : 3
}
],
"ok" : 1
}
but expected:
{
"result" : [
{
"_id" : null,
"all" : 3,
"order" : 1
}
],
"ok" : 1
}
It makes no difference what I'll put - "$reports.order", "$reports.xxx", etc, aggregation framework check only if the field reports exists, ignores embed.
$ifNull and $eq dosn't work with embeded documents?
Is any way to do something like this
db.test.find({"reports.order": {$exists: 1}})
in aggregation framework?
Sorry for my english and I hope that you understood what I want to show you :)
I think it doesn't work because the field "reports" contain an array, not an object.
I mean, your aggregation works as you expect in this collection:
>db.test.find()
{
"_id": ObjectId("54906479e89cdf95f5fb2351"),
"reports":
{
"desc": "xxx",
"order": {"$id": ObjectId("53fbede62827b89e4f86c12e")}
}
},
{
"_id": ObjectId("54906515e89cdf95f5fb2352"),
"reports":
{
"desc": "xxx"
}
},
{
"_id": ObjectId("549067d3e89cdf95f5fb2353"),
"reports":
{
"desc": "xxx"
}
}
Note that I removed the "[" and "]", so now it's an object, not an array (one-to-one relation).
Because you have array inside the "report" field, you need to unwind the array to output one document for each element. I suppose that if you have two "order" fields inside the "reports" array, you only wants to count it once. I mean:
"reports": [
{
"desc": "xxx",
"order": {"$id": ObjectId("53fbede62827b89e4f86c12e")},
"order": "yyy",
}
]
Should only count as one for the object final "order" sum.
In this case, you need to unwind, group by _id (because the previous example outputs two documents for the same _id) and then group again to count all documents:
db.test.aggregate([
{$unwind: '$reports'},
{$group:{
_id:"$_id",
order:{$sum:{"$cond": [
{
"$ifNull": ["$reports.order", false]
},
1,
0
]
}
}
}},
{$group:{
_id:null,
all:{$sum:1},
order: {
$sum:{
"$cond": [{$eq: ['$order', 0]}, 0, 1]
}
}
}}])
Maybe there is a shorter solution, but this works.

mongodb get filtered count

I have no extended knowledge on how to create mongodb queries, but I wanted to ask how could I query collection get something like this:
{
Total: 1000,
Filtered: 459,
DocumentArray: []
}
Of course doing that in one query, so I do not need to do something like this:
db.collection.find();
db.collection.find().count();
db.colection.count();
Well you could do something along these lines:
Considering documents like this:
{ "_id" : ObjectId("531251829df82824bdb53578"), "name" : "a", "type" : "b" }
{ "_id" : ObjectId("531251899df82824bdb53579"), "name" : "a", "type" : "c" }
{ "_id" : ObjectId("5312518e9df82824bdb5357a"), "type" : "c", "name" : "b" }
And an aggregate pipeline like this:
db.collection.aggregate([
{ "$group": {
"_id": null,
"totalCount": { "$sum": 1 },
"docs": { "$push": {
"name": "$name",
"type": "$type"
}},
}},
{ "$unwind": "$docs" },
{ "$match": { "docs.name": "a" } },
{ "$group": {
"_id": null,
"totalCount": { "$first": "$totalCount" },
"filteredCount": { "$sum": 1 },
"docs": { "$push": "$docs" }
}}
])
But I would not recommend it. It will certainly blow up on any "real" collection due to exceeding the maximum BSON document size. And I would doubt it would be performing very well. But that is how it can be done, even if the utility is purely academic.
Just do what you are doing if you need the information. That is the "right way" to do it.