I'm working on a mongoDB query.
I have several documents which I query with following results:
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:20:00.000Z"),
"value" : true
}
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:26:00.000Z"),
"value" : false
}
{
"_id" : 1000.0,
"date" : ISODate("2018-05-25T00:30:00.000Z"),
"value" : false
}
The original documents are filtered so that I get only document within the last 15 minutes before now and there is no way of knowing how many entries are in that time range.
I need to expand my existing query so that it returns a status based on the "value". If there are no true I need a status 0, if there is at least 1 but not only true I need a status 1, and if there are only true I need a status 2.
For example:
{
"_id" : 1000,
"status" : 1
},
{
"_id" : 1001,
"status" : 2
}
Is there a way of accomplishing this using mongoDB? Or would it be better/easier to do it on java side? Note that there are several _id in the database.
You can gather all values from each group into one array (using $group and $push) and then use $switch to apply your logic. To determine whether array contains any true value or all values are true you can use $anyElementTrue and $allElementsTrue:
db.col.aggregate([
{
$group: {
_id: "$_id",
values: { $push: "$value" }
}
},
{$unwind:"$values"},
{
$project: {
_id: 1,
status: {
$switch: {
branches: [
{ case: { $allElementsTrue: "$values" }, then: 2 },
{ case: { $anyElementTrue: "$values" }, then: 1 },
],
default: 0
}
}
}
}
])
Related
I have a document like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 105.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 110.0,
"dateTime" : ISODate("2021-05-05T09:18:08.000Z")
},
{
"paramVal" : 115.0,
"dateTime" : ISODate("2021-05-05T10:18:08.000Z")
},
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}
Now I need to filter a document which I can do easily with match or find but in that document the subarray i.e. values should have latest 2 values because in future the count can be more than 100.
the output should be like
{
"deviceId" : "1106",
"orgId" : "5ffe9fe1c9e77c0006f0aad3",
"values" : [
{
"paramVal" : 125.0,
"dateTime" : ISODate("2021-05-05T11:18:08.000Z")
},
{
"paramVal" : 135.0,
"dateTime" : ISODate("2021-05-05T12:18:08.000Z")
}
]
}
Try $slice operator, to select number of elements, pass negative value to select documents from below/last elements,
db.collection.aggregate([
{ $set: { values: { $slice: ["$values", -2] } } }
])
Playground
I need for the array values in sorted order by date
There is no straight way to do this, check the below aggregation query, but it will cause the performance issues, i would suggest to change you schema structure to manage this data order by date,
$unwind deconstruct values array
$sort by dateTime in descending order
$group by _id and reconstruct values array and return other required fields
$slice to select number of elements, pass negative value to select documents from below/last elements
db.collection.aggregate([
{ $unwind: "$values" },
{ $sort: { "values.dateTime": -1 } },
{
$group: {
_id: "$_id",
deviceId: { $first: "$deviceId" },
orgId: { $first: "$orgId" },
values: { $push: "$values" }
}
},
{ $set: { values: { $slice: ["$values", 2] } } }
])
Playground
I want to get the order of some user from a list after $sort aggregation pipeline.
Let's say we have a leaderboard, and I need to get my rank in the leaderboard with only one query getting only my data.
I have tried $addFields and some queries with $map
Let's say we have these documents
/* 1 createdAt:8/18/2019, 4:42:41 PM*/
{
"_id" : ObjectId("5d5963e1c6c93b2da849f067"),
"name" : "x4",
"points" : 69
},
/* 2 createdAt:8/18/2019, 4:42:41 PM*/
{
"_id" : ObjectId("5d5963e1c6c93b2da849f07b"),
"name" : "x24",
"points" : 968
},
/* 3 createdAt:8/18/2019, 4:42:41 PM*/
{
"_id" : ObjectId("5d5963e1c6c93b2da849f06a"),
"name" : "x7",
"points" : 997
},
And I want to write a query like this
db.table.aggregate(
[
{ $sort : { points : 1 } },
{ $addFields: { order : "$index" } },
{ $match : { name : "x24" } }
]
)
I need to inject the order field with something like $index
I expect to have something like this in return
{
"_id" : ObjectId("5d5963e1c6c93b2da849f07b"),
"name" : "x24",
"points" : 968,
"order" : 2
}
I need something like the metadata of the result here which return 2
/* 2 createdAt:8/18/2019, 4:42:41 PM*/
One of the workaround for this situation is to convert your all documents into one single array and hence resolve the index of the document using this array with help of $unwind and finally project the data with fields as required.
db.collection.aggregate([
{ $sort: { points: 1 } },
{
$group: {
_id: 1,
register: { $push: { _id: "$_id", name: "$name", points: "$points" } }
}
},
{ $unwind: { path: "$register", includeArrayIndex: "order" } },
{ $match: { "register.name": "x4" } },
{
$project: {
_id: "$register._id",
name: "$register.name",
points: "$register.points",
order: 1
}
}
]);
To make it more efficient you can apply limit, match, and filter as per your requirement.
I have the following document:
"_id" : 19,
"name" : "Elizabeth Moore",
"achronym" : "EM19",
"calc" : {
"20" : {
"role" : 20,
"score" : 15,
"inRole" : false,
"range" : {
"int" : 80,
"min" : 20
}
and I need to retrieve all _ids having "calc.inRole" false.
I tried:
db.coll.find({'calc.$.inRole': false})
db.coll.find({'calc.inRole': false})
but none of these worked.
How can I achieve that?
Since calc has fields with unknown keys you need to run $objectToArray to transofrm it into array of keys and values. Then you can run $in on that array. If you want to have it as single pipeline step you can use $let operator to define temporary variable:
db.collection.aggregate([
{
$match: {
$expr:{
$let: {
vars: {
arr: { $objectToArray: "$calc" }
},
in: {
$in: [ false, "$$arr.v.inRole" ]
}
}
}
}
},
{
$project: {
_id: 1
}
}
])
Mongo Playground
I have a mongo collection containing structurally similar documents as illustrated below-
{
"_id" : ObjectId("mongoid"),
"type" : "chemical",
"sourceId" : "27553452120",
"array1" : [
{
"cid" : "1235689",
"outcome" : "test",
"relation" : "=",
"array2" : [
{
"name" : "test1"
},
{
"name" : "test2"
},
{
"value" : 1.628,
"name" : "test3"
},
{
"value" : 1.63,
"name" : "test4"
}
]
}
]
}
I want to query this collection for a case, where array1.array2.length > 1
I tried following sample query on mongo shell:
db.collection.find({"array1.array2":{$exists:true},$where:"this.array1.array2.length>1"}).limit(1).pretty()
but it fails stating
Error: error: {
"ok" : 0,
"errmsg" : "TypeError: this.array1.array2 is undefined :\n#:1:15\n",
"code" : 139,
"codeName" : "JSInterpreterFailure"
}
How can this query be achieved?
This should solve your problem.
db.test.aggregate(
{
$match:{"array1.array2": {$nin:[null,[]]}}
}
).pretty();
Try this
db.collection.find({"array1.array2":{$exists: true, $ne : []} })
According to description as mentioned into above question please try executing following aggregate query as a solution.
db.collection.aggregate(
// Pipeline
[
// Stage 1
{
$match: {
array1: {
$elemMatch: {
array2: {
$exists: true
}
}
}
}
},
// Stage 2
{
$project: {
array2size: {
$size: {
$arrayElemAt: ['$array1.array2', 0]
}
},
array1: 1
}
},
// Stage 3
{
$match: {
'array2size': {
$gt: 0
}
}
},
]
);
What about this way:
db.test.find({"array1.array2.1":{$exists:1}})
This is more flexible, it allow you to query for any length not only 1.
Last ".1" it is zero based array index. So if index 1 exists it means array has at least 2 elements. You can search for < N by changing $exists to 0. If no index 3 in array it means array has less then 4 elements.
If you need to search for exact size you can use $size operator
db.test.find({"array1.array2":{$size:5}})
On Mongo 2.4.6
Collection of Users
{
"_id" : User1,
"orgRoles" : [
{"_id" : 1, "app" : "ANGRYBIRDS", "orgId" : "CODOE"},
{"_id" : 2, "app" : "ANGRYBIRDS", "orgId" : "MSDN"}
],
},
{
"_id" : User2,
"orgRoles" : [
{"_id" : 1, "app" : "ANGRYBIRDS", "orgId" : "CODOE"},
{"_id" : 2, "app" : "HUNGRYPIGS", "orgId" : "MSDN"}
],
},
{
"_id" : User2,
"orgRoles" : [
{"_id" : 1, "app" : "ANGRYBIRDS", "orgId" : "YAHOO"},
{"_id" : 2, "app" : "HUNGRYPIGS", "orgId" : "MSDN"}
],
}
With data that looks like above, I'm trying to write a query to get:
All the id's of the users that have only one ANGRYBIRDS app and that ANGRYBIRDS app is in the CODOE organization.
So it would return User2 because they have 1 ANGRYBIRDS and is in the ORG "CODOE" but not User1 because they have two ANGRYBIRDS or User3 because they don't have an ANGRYBIRDS app in the "CODOE" organization. I'm fairly new to mongo queries, so any help is appreciated.
To do something with a few more detailed conditions not immediately offered by standard operators, then your best approach is to use the aggregation framework. This allows you do some processing to work our your conditions, such as the number of matches:
db.collection.aggregate([
// Filter the documents that are possible matches
{ "$match": {
"orgRoles": {
"$elemMatch": {
"app": "ANGRYBIRDS", "orgId": "CODOE"
}
}
}},
// De-normalize the array content
{ "$unwind": "$orgRoles" },
// Group and count the matches
{ "$group": {
"_id": "$_id",
"orgRoles": { "$push": "$orgRoles" },
"matched": {
"$sum": {
"$cond": [
{ "$eq": ["$orgRoles.app", "ANGRYBIRDS"] },
1,
0
]
}
}
}},
// Filter where matched is more that 1
{ "$match": {
"orgRoles": {
"$elemMatch": {
"app": "ANGRYBIRDS", "orgId": "CODOE"
}
},
"matched": 1
}},
// Optionally project to just keep the original fields
{ "$project": { "orgRoles": 1 } }
])
The main thing here happens after the initial $match is processed to only return those documents that have at least one array element matching the main condition, and then after the array elements are processed with $unwind so they can be inspected individually.
The trick is the conditional $sum operation with the $cond operator which is a "ternary". This evaluates "howMany" matches were found in the array to the "ANGRYBIRDS" string. Following this you $match again in order to "filter" any documents that had a match count of more than one. Still leaving the other condition in there, but that is really not necessary.
Just for the record, this is also possible with using the JavaScript evaluation of the $where clause, but due to that it is likely not to be as efficient at processing:
db.collection.find({
"orgRoles": {
"$elemMatch": {
"app": "ANGRYBIRDS", "orgId": "CODOE"
}
},
"$where": function() {
var orgs = this.orgRoles.filter(function(el) {
return el.app == "ANGRYBIRDS";
});
return ( orgs.length == 1 );
}
})
One way of doing it using the aggregation pipeline is:
db.users.aggregate([
// Match the documents with app being "ANGRYBIRDS" and orgID being "CODE"
// Note that this step filters out most of the documents and is good to have
// at the start of the pipeline, moreover it can make use of indexes, if
// used at the beginning of the aggregation pipeline.
{
$match : {
"orgRoles.app" : "ANGRYBIRDS",
"orgRoles.orgId" : "CODOE"
}
},
// unwind the elements in the orgRoles array
{
$unwind : "$orgRoles"
},
// group by userid and app
{
$group : {
"_id" : {
"id" : "$_id",
"app" : "$orgRoles.app"
},
// take the id and app of the first document in each group, since all
// the
// other documents in the group will have the same values.
"id" : {
$first : "$_id"
},
"app" : {
$first : "$orgRoles.app"
},
// orgId can be different, so form an array for each group.
"orgId" : {
$push : {
"id" : "$orgRoles.orgId"
}
},
// count the number of documents in each group.
"count" : {
$sum : 1
}
}
},
// find the matching group
{
$match : {
"count" : 1,
"app" : "ANGRYBIRDS",
"orgId" : {
$elemMatch : {
"id" : "CODOE"
}
}
}
},
// project only the userid
{
$project : {
"id" : 1,
"_id" : 0
}
} ]);
Edit: Removed mapping the aggregation result, since the problem requires solution in v2.4.6, and according to the documentation.
Changed in version 2.6: The db.collection.aggregate() method returns a cursor and can return result sets of any size. Previous versions
returned all results in a single document, and the result set was
subject to a size limit of 16 megabytes.