MongoDB Query group and distinct together - mongodb

Consider the following set of documents:
[
{
"name" : "nameA",
"class" : "classA",
"age" : 24,
"marks" : 45
},
{
"name" : "nameB",
"class" : "classB",
"age" : 22,
"marks" : 65
},
{
"name" : "nameC",
"class" : "classA",
"age" : 14,
"marks" : 55
}
]
I need to fetch the min and max values for age and marks as well as the distinct values for name and class.
I know that I can use aggregate and group to get the max and min values of age and marks with one query, and I can get distinct values of name and class using distinct query.
But I don't want to do separate queries to fetch that information. Is there a way in which I can get the result with one query? Let's say if I can merge the aggregate and distinct somehow.

Sure, you can do it with one aggregation command. You need to use $group with $addToSet operator:
db.collection.aggregate([{
$group : {
_id : null,
name : { $addToSet : "$name" },
class : { $addToSet : "$class" },
ageMin : { $min : "$age" },
ageMax : { $max : "$age" },
marksMin : { $min : "$marks" },
marksMax : { $max : "$marks" }
}
}]);
$addToSet will create an array with unique values for the selected field.
This aggregation will return the following response for your example docs:
{
"_id" : null,
"name" : [
"nameC",
"nameB",
"nameA"
],
"class" : [
"classB",
"classA"
],
"ageMin" : 14,
"ageMax" : 24,
"marksMin" : 45,
"marksMax" : 65
}

Related

Find field inside an array using $elemMatch

I have a invoice collection, in which I want find the document with a specified book's id.
db.invoice.find({"sold": {$elemMatch: {"book":{$elemMatch:{"_id":"574e68e5ac9fbac82489b689"}}}}})
I tried this but it didn't work
{
"_id" : ObjectId("575e9bf5576533313ac9d993"),
"sold" : [
{
"book" : {
"_id" : "574e68e5ac9fbac82489b689",
"price" : 100,
},
"quantity" : 10,
"total_price" : 1000
}
],
"date" : "13-06-2016"
}
You do not need the $elemMatch query operator here because you only specify only a single query condition.
db.invoice.find( { 'sold.book._id': '574e68e5ac9fbac82489b689' } )
This is mentioned in the documentation:
If you specify only a single condition in the $elemMatch expression, you do not need to use $elemMatch
https://docs.mongodb.com/manual/reference/operator/query/elemMatch/#op._S_elemMatch
The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria.
mongo> db.invoice.find({"sold": {$elemMatch: {"book._id":"574e68e5ac9fbac82489b689"}}}).pretty()
{
"_id" : ObjectId("575e9bf5576533313ac9d993"),
"sold" : [
{
"book" : {
"_id" : "574e68e5ac9fbac82489b689",
"price" : 100
},
"quantity" : 10,
"total_price" : 1000
}
],
"date" : "13-06-2016"
}

How to create an index on the "name" field of a document in mongodb

I want to create an index on the name field of a document in mongodb so that when I do a find,I should get all the names to be displayed in the alphabetical order.How can I achieve this ? Can anyone please help me out ...
My documents in mongodb:
db.col.find();
{ "_id" : ObjectId("5696256b0c50bf42dcdfeae1"), "name" : "Daniel", "age" : 24 }
{ "_id" : ObjectId("569625850c50bf42dcdfeae2"), "name" : "Asha", "age" : 21 }
{ "_id" : ObjectId("569625a40c50bf42dcdfeae3"), "name" : "Hampi", "age" : 34 }
{ "_id" : ObjectId("5696260f0c50bf42dcdfeae5"), "name" : "Bhavana", "age" : 14 }
Actually you don't need an index in order to display your result alphabetically. What you need is the .sort() method.
db.collection.find().sort({'name': 1})
Which returns
{ "_id" : ObjectId("569625850c50bf42dcdfeae2"), "name" : "Asha", "age" : 21 }
{ "_id" : ObjectId("5696260f0c50bf42dcdfeae5"), "name" : "Bhavana", "age" : 14 }
{ "_id" : ObjectId("5696256b0c50bf42dcdfeae1"), "name" : "Daniel", "age" : 24 }
{ "_id" : ObjectId("569625a40c50bf42dcdfeae3"), "name" : "Hampi", "age" : 34 }
Creating an index on a field in your document will not automatically sort your result on that particular field you still need to use the .sort() method. see Use Indexes to Sort Query Results
If you want to return an array of all names in your documents in ascending order then you will need to use the .aggregate() method.
The first stage in the pipeline is the $sort stage where you sort your documents by "name" in ascending order. The last stage is the $group stage where you group your documents and use the $push accumulator operator to return an array of "names"
db.collection.aggregate([
{ "$sort": { "name": 1 } },
{ "$group": { "_id": null, "names": { "$push": "$name" } } }
])
Which yields:
{ "_id" : null, "names" : [ "Asha", "Bhavana", "Daniel", "Hampi" ] }

Aggregation $group return fields as string and not as array

I'm doing the following aggregation:
db.col.aggregate([
{'$unwind': "$students"},
{'$group':
{
"_id" : "$_id",
'students' :
{ '$push' :
{
'name' : '$students.name',
'school' : '$students.school',
'age' : '$students.age',
}
},
'zipcode' :
{'$addToSet':
'$zipcode'
}
}
},
{'$project':
{
'_id' : 0 ,
'students' : 1,
'zipcode': 1
}
}
])
Which gives:
{
"result" : [
{
"students" : [{
"name" : "john",
"school" : 102,
"age" : 10
},
{
"name" : "jess",
"school" : 102,
"age" : 11
},
{
"name" : "barney",
"school" : 102,
"age" : 7
}
],
"zipcode" : [63109]
}
],
"ok" : 1
}
Is it possible to have make it return "zipcode" : 63109?
In practice this is what I want to have a returning result of the aggregation:
{
"result" : [
{
"students" : [{
"name" : "john",
"school" : 102,
"age" : 10
},
{
"name" : "jess",
"school" : 102,
"age" : 11
},
{
"name" : "barney",
"school" : 102,
"age" : 7
}
],
"zipcode" : 63109
}
],
"ok" : 1
}
I tried in the $group to have "zipcode" : "$zipcode" but as the documentation says:
Every $group expression must specify an _id field.
In addition to the _id field, $group expression can include
computed fields. These other fields must use one of the following accumulators:
$addToSet
$first
$last
$max
$min
$avg
$push
$sum
Is there any workaround?
The zipcode value is returned as an array because you are using the $addToSet operator, which explicitly returns an array:
Returns an array of all the values found in the selected field among the documents in that group.
If your intention is actually to group by zipcode, you can instead use this as the grouping _id, eg:
db.col.aggregate([
// Create a stream of documents from the students array
{'$unwind': "$students"},
// Group by zipcode
{'$group':
{
"_id" : "$zipcode",
'students' :
{ '$push' :
{
'name' : '$students.name',
'school' : '$students.school',
'age' : '$students.age',
}
},
}
},
// rename _id to zipcode
{'$project':
{
'_id' : 0,
'zipcode' : '$_id',
'students' : 1,
}
}
])
Alternatively, you could use a group operator such as $first or $last to return a single zipcode value, but this probably isn't what you're after.

aggregate request MongoDB

I'd like get a multiple fields in a collection list with a condition. I tried a aggregate request but i have an error.
My request
db.people.aggregate({$match:{createdDate:{$exists:true},"ad":"noc2"}},{$group:{value2:$value2}});
My Json :
db.test.findOne();
{
"_id" : ObjectId("51e7dd16d2f8db27b56ea282"),
"ad" : "noc2",
"list" : {
"p45" : {
"id" : "p45",
"date" : ISODate("2014-01-01T12:18:30.568Z"),
"value3" : 21,
"value1" : 100,
"value2" : 489
},
"p6" : {
"id" : "p6"
"date" : ISODate("2013-07-18T12:18:30.568Z"),
"value3" : 21,
"value1" : 100,
"value2" : 489
},
"p4578" : {
"id" : "4578"
"date" : ISODate("2013-07-18T12:18:30.568Z"),
"value3" : 21,
"value1" : 100,
"value2" : 489
}
}
}
I want to get this json, for example, in result :
{id:p45,value:587},{id:p4578,value:47},{id:p6,value:2}
There are several issues with your sample document and aggregation:
the sample doc will not match your aggregation query because you are matching on createdDate field existing
the $group() operator works on a document level and needs an _id field to group by
your list field is an embedded document, not an array
aside from formatting, there is no obvious way to relate the sample values to the calculated result you are looking for
Here is an adjusted example document with the list as an array as well as some unique values for each item that happen to add up to the value numbers you mentioned:
db.people.insert({
"ad" : "noc2",
"createdDate" : ISODate(),
"list" : [
{
"id" : "p45",
"date" : ISODate("2014-01-01T12:18:30.568Z"),
"value3" : 21,
"value1" : 77,
"value2" : 489
},
{
"id" : "p6",
"date" : ISODate("2013-07-18T12:18:30.568Z"),
"value3" : 20,
"value1" : 20,
"value2" : 7
},
{
"id" : "4578",
"date" : ISODate("2013-07-18T12:18:30.568Z"),
"value3" : 21,
"value1" : 300,
"value2" : -319
}
]
})
You can now use the $unwind operator to extract the matching subdocuments.
It is unclear from the current question description what $group operation you are trying to achieve and whether you actually need $group.
Here is an example using the Aggregation Framework to $add (total) the three values for each list item:
db.people.aggregate(
// Find relevant docs (can use index if available)
{ $match: {
createdDate: { $exists:true },
"ad":"noc2"
}},
// Convert list array to stream of documents
{ $unwind: "$list" },
// Add (value1, value2, value3) for each list item
{ $project: {
_id: "$list.id",
value: { $add: [ "$list.value1", "$list.value2", "$list.value3"] }
}}
);
Sample output:
{
"result" : [
{
"_id" : "p45",
"value" : 587
},
{
"_id" : "p6",
"value" : 47
},
{
"_id" : "4578",
"value" : 2
}
],
"ok" : 1
}
Note that I've only aggregated using a single document for this example, and the output will be one result document for every item in the list array.
In real usage you may want to add an additional aggregation step to $group by document _id or some other criteria, depending on the outcome you are trying to achieve.

mongodb get distinct records

I am using mongoDB in which I have collection of following format.
{"id" : 1 , name : x ttm : 23 , val : 5 }
{"id" : 1 , name : x ttm : 34 , val : 1 }
{"id" : 1 , name : x ttm : 24 , val : 2 }
{"id" : 2 , name : x ttm : 56 , val : 3 }
{"id" : 2 , name : x ttm : 76 , val : 3 }
{"id" : 3 , name : x ttm : 54 , val : 7 }
On that collection I have queried to get records in descending order like this:
db.foo.find({"id" : {"$in" : [1,2,3]}}).sort(ttm : -1).limit(3)
But it gives two records of same id = 1 and I want records such that it gives 1 record per id.
Is it possible in mongodb?
There is a distinct command in mongodb, that can be used in conjunction with a query. However, I believe this just returns a distinct list of values for a specific key you name (i.e. in your case, you'd only get the id values returned) so I'm not sure this will give you exactly what you want if you need the whole documents - you may require MapReduce instead.
Documentation on distinct:
http://www.mongodb.org/display/DOCS/Aggregation#Aggregation-Distinct
You want to use aggregation. You could do that like this:
db.test.aggregate([
// each Object is an aggregation.
{
$group: {
originalId: {$first: '$_id'}, // Hold onto original ID.
_id: '$id', // Set the unique identifier
val: {$first: '$val'},
name: {$first: '$name'},
ttm: {$first: '$ttm'}
}
}, {
// this receives the output from the first aggregation.
// So the (originally) non-unique 'id' field is now
// present as the _id field. We want to rename it.
$project:{
_id : '$originalId', // Restore original ID.
id : '$_id', //
val : '$val',
name: '$name',
ttm : '$ttm'
}
}
])
This will be very fast... ~90ms for my test DB of 100,000 documents.
Example:
db.test.find()
// { "_id" : ObjectId("55fb595b241fee91ac4cd881"), "id" : 1, "name" : "x", "ttm" : 23, "val" : 5 }
// { "_id" : ObjectId("55fb596d241fee91ac4cd882"), "id" : 1, "name" : "x", "ttm" : 34, "val" : 1 }
// { "_id" : ObjectId("55fb59c8241fee91ac4cd883"), "id" : 1, "name" : "x", "ttm" : 24, "val" : 2 }
// { "_id" : ObjectId("55fb59d9241fee91ac4cd884"), "id" : 2, "name" : "x", "ttm" : 56, "val" : 3 }
// { "_id" : ObjectId("55fb59e7241fee91ac4cd885"), "id" : 2, "name" : "x", "ttm" : 76, "val" : 3 }
// { "_id" : ObjectId("55fb59f9241fee91ac4cd886"), "id" : 3, "name" : "x", "ttm" : 54, "val" : 7 }
db.test.aggregate(/* from first code snippet */)
// output
{
"result" : [
{
"_id" : ObjectId("55fb59f9241fee91ac4cd886"),
"val" : 7,
"name" : "x",
"ttm" : 54,
"id" : 3
},
{
"_id" : ObjectId("55fb59d9241fee91ac4cd884"),
"val" : 3,
"name" : "x",
"ttm" : 56,
"id" : 2
},
{
"_id" : ObjectId("55fb595b241fee91ac4cd881"),
"val" : 5,
"name" : "x",
"ttm" : 23,
"id" : 1
}
],
"ok" : 1
}
PROS: Almost certainly the fastest method.
CONS: Involves use of the complicated Aggregation API. Also, it is tightly coupled to the original schema of the document. Though, it may be possible to generalize this.
I believe you can use aggregate like this
collection.aggregate({
$group : {
"_id" : "$id",
"docs" : {
$first : {
"name" : "$name",
"ttm" : "$ttm",
"val" : "$val",
}
}
}
});
The issue is that you want to distill 3 matching records down to one without providing any logic in the query for how to choose between the matching results.
Your options are basically to specify aggregation logic of some kind (select the max or min value for each column, for example), or to run a select distinct query and only select the fields that you wish to be distinct.
querymongo.com does a good job of translating these distinct queries for you (from SQL to MongoDB).
For example, this SQL:
SELECT DISTINCT columnA FROM collection WHERE columnA > 5
Is returned as this MongoDB:
db.runCommand({
"distinct": "collection",
"query": {
"columnA": {
"$gt": 5
}
},
"key": "columnA"
});
If you want to write the distinct result in a file using javascript...this is how you do
cursor = db.myColl.find({'fieldName':'fieldValue'})
var Arr = new Array();
var count = 0;
cursor.forEach(
function(x) {
var temp = x.id;
var index = Arr.indexOf(temp);
if(index==-1)
{
printjson(x.id);
Arr[count] = temp;
count++;
}
})
Specify Query with distinct.
The following example returns the distinct values for the field sku, embedded in the item field, from the documents whose dept is equal to "A":
db.inventory.distinct( "item.sku", { dept: "A" } )
Reference: https://docs.mongodb.com/manual/reference/method/db.collection.distinct/