query the before last object in array - mongodb

I'm using MongoDB and i have the following records:
{
name: "a",
status: [
{ age: 15, closed: true},
{ age: 38, closed: true},
{ age: 42, closed: false},
]
},
{
name: "b",
status: [
{ age: 29, closed: true},
{ age: 5, closed: false},
]
}
I want to check if the before last object in status has for example age = 29.
is there anyway to do that without using aggregation ?

You can use $arrayElemAt and pass -2 as an argument to get before last element (-1 represents last one),
If negative, $arrayElemAt returns the element at the idx position, counting from the end of the array.
Try:
db.col.aggregate([
{
$addFields: {
beforeLast: { $arrayElemAt: [ "$status", -2 ] }
}
},
{
$match: {
"beforeLast.age": 29
}
},
{
$project: {
beforeLast: 0
}
}
])
EDIT: alternatively you can use $expr with $let keyword to define a temporary variable:
db.col.find({
$expr: {
$eq: [
29,
{
$let: {
vars: { beforeLast: { $arrayElemAt: [ "$status", -2 ] } },
in: "$$beforeLast.age"
}
}
]
}
})

Related

How to get several combinations of documents where sums of properties reach a certain value in Mongodb?

If we imagine this kind of document structure :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
},
{
id: 4,
name: "",
values : {
a: 89,
b: 2
}
}
// ...
]
Is it possible to get one or more lists of documents where, for example, the sum of the $.values.a equals 100 and the sum of the $.values.b equals 120? Or if not is it possible to sort the bests fits with a kind of threshold?
For example, the best output can be something like that :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
}
]
There is no any native implementation...
But, You can have desired results if your data meets some requirements:
You collection has no too much data (this solution scales badly)
Your id field is unique
Your collection has index for id field
Explanation
We sort by id
With $lookup with the same collection (it's important ´id´ to be indexed) and pick next 10 documents for the current document L i=(Doc i+1 ... Doc i+11)
With $reduce, we count from i ... i+n untill a > 100 and b > 120
With $facet, we separate lists which meets exactly a=100, b=120 results (equals) and threshold (+- 10 for values.a and values.b)
Last steps, if any equals exists, we ignore threshold. Otherwise, we take threshold.
db.collection.aggregate([
{
$sort: {
id: 1
}
},
{
$lookup: {
from: "collection",
let: {
id: "$id"
},
pipeline: [
{
$sort: {
id: 1
}
},
{
$match: {
$expr: {
$gt: [
"$id",
"$$id"
]
}
}
},
{
$limit: 10
}
],
as: "bucket"
}
},
{
$replaceRoot: {
newRoot: {
$reduce: {
input: "$bucket",
initialValue: {
a: "$values.a",
b: "$values.b",
data: [
{
_id: "$_id",
id: "$id",
name: "$name",
values: "$values"
}
]
},
in: {
a: {
$add: [
"$$value.a",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.a",
0
]
}
]
},
b: {
$add: [
"$$value.b",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.b",
0
]
}
]
},
data: {
$concatArrays: [
"$$value.data",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
[
"$$this"
],
[]
]
}
]
}
}
}
}
}
},
{
$facet: {
equals: [
{
$match: {
a: 100,
b: 120
}
}
],
threshold: [
{
$match: {
a: {
$gte: 90,
$lt: 110
},
b: {
$gte: 110,
$lt: 130
}
}
}
]
}
},
{
$project: {
result: {
$cond: [
{
$gt: [
{
$size: "$equals"
},
0
]
},
"$equals",
"$threshold"
]
}
}
},
{
$unwind: "$result"
}
])
MongoPlayground

Query Mongo to get only documents different that previous document [duplicate]

Im wondering if the following is possible in MongoDB.
I have collection of documents that represent changes in some value in time:
{
"day" : ISODate("2018-12-31T23:00:00.000Z"),
"value": [some integer value]
}
There are no 'holes' in the data, I have entries for all days within some period.
Is it possible to query this collection to get only documents that has different value than previous one (when sorting by day asc)? For example, having following documents:
{ day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 }
I want to retrieve documents for 2018-04-01, 2018-04-03 and 2018-04-06 and only those since others don't have a change of value.
You need to get pairs of consecutive docs to detect the gap. For that you can push all documents into single array, and zip it with itself shifted 1 element from the head:
db.collection.aggregate([
{ $sort: { day: 1 } },
{ $group: { _id: null, docs: { $push: "$$ROOT" } } },
{ $project: {
pair: { $zip: {
inputs:[ { $concatArrays: [ [false], "$docs" ] }, "$docs" ]
} }
} },
{ $unwind: "$pair" },
{ $project: {
prev: { $arrayElemAt: [ "$pair", 0 ] },
next: { $arrayElemAt: [ "$pair", 1 ] }
} },
{ $match: {
$expr: { $ne: ["$prev.value", "$next.value"] }
} },
{ $replaceRoot:{ newRoot: "$next" } }
])
The rest is trivial - you unwind the array back to documents, compare the pairs, filter out the equal ones, and replaceRoot from what's left.
Starting in Mongo 5, it's a perfect use case for the new $setWindowFields aggregation operator:
// { day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 } <=
// { day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 } <=
// { day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 } <=
db.collection.aggregate([
{ $setWindowFields: {
sortBy: { day: 1 },
output: { pair: { $push: "$value", window: { documents: [-1, "current"] } } }
}},
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10, pair: [ 10 ] }
// { day: ISODate("2019-04-02T00:00:00Z"), value: 10, pair: [ 10, 10 ] }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15, pair: [ 10, 15 ] }
// { day: ISODate("2019-04-04T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-05T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10, pair: [ 15, 10 ] }
{ $match: { $expr: { $or: [
{ $eq: [ { $size: "$pair" }, 1 ] }, // first doc doesn't have a previous doc
{ $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }
]}}},
{ $unset: ["pair"] }
])
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10 }
This:
starts with a $setWindowFields aggregation stage which adds a pair field representing the current document's value and the previous document's value (output: { pair: { ... }}):
$setWindowFields provides for a given document a view of other documents (a window)
which in our case is the "current" document and the previous one "-1": window: { documents: [-1, "current"] }.
such that we build within this window an array of values: $push: "$value"
and note that we've made sure to sort documents by day: sortBy: { day: 1 }.
and then:
filters in the first document (which is remarquable by its array having only one element): { $eq: [ { $size: "$pair" }, 1 ] }
and filters out the following documents if their pair has the same values: { $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }

how to convert query to use regex instead of equal

I'm using MongoDB and i have the following records:
{
name: "a",
status: [
{ age: 15, closed: true},
{ age: 38, closed: true},
{ age: 42, closed: false},
]
},
{
name: "b",
status: [
{ age: 29, closed: true},
{ age: 5, closed: false},
]
}
I want to check if the before last object in status has for example age = 29.
so for that i have this working query:
db.col.find({
$expr: {
$eq: [
29,
{
$let: {
vars: { beforeLast: { $arrayElemAt: [ "$status", -2 ] } },
in: "$$beforeLast.age"
}
}
]
}
})
but i want now to check for example if age contain a value like "2". i need to use regex expression. is there anyway to convert that query to use regex inside/instead $eq operator ?
PS: i don't want to use aggregation because i'm working with an old version.
You can use $indexOfCP to find sub string inside an string character
db.collection.find({
"$expr": {
"$ne": [
{
"$indexOfCP": [
{ "$toLower": {
"$let": {
"vars": {
"beforeLast": {
"$arrayElemAt": [
"$status",
-2
]
}
},
"in": "$$beforeLast.age"
}
}},
"2"
]
},
-1
]
}
})
Try with aggregation
db.collection.aggregate([
{
$project:
{
last: { $arrayElemAt: [ "$status", -2 ] },
}
},
{$addFields:{
ageInString: { $substr: [ "$last.age", 0, 3 ] }
}},
{$match:{ageInString:{ $regex: '^2' }}}
])

Mongo group aggregation with priority of record

I am trying to perform a MongoDB 3.6 aggregation and I can't figure out the right way.
The problem is following. After performing several aggregation steps I end up with result set like this:
[
{ _id: { month: 1, type: 'estimate' }, value: 50 },
{ _id: { month: 2, type: 'estimate' }, value: 40 },
{ _id: { month: 3, type: 'estimate' }, value: 35 },
{ _id: { month: 3, type: 'exact' }, value: 33.532 },
{ _id: { month: 4, type: 'estimate' }, value: 10 },
{ _id: { month: 4, type: 'exact' }, value: 11.244 },
]
It contains values grouped by month. Value for every month can be 'estimated' or 'exact'. Now I would like to reduce this result to achieve this:
[
{ _id: { month: 1 }, value: 50 },
{ _id: { month: 2 }, value: 40 },
{ _id: { month: 3 }, value: 33.532 },
{ _id: { month: 4 }, value: 11.244 },
]
Basically I want to use the value of type 'exact' whenever it's possible and only fallback to 'estimate' value in months where the 'exact' is not known.
Any help or tip will be greatly appreciated. I would like to perform that aggregation in the DB not on server.
You can simply $sort by type and then take use $first in next $group stage which will give you exact if exists and estimate otherwise. Try:
db.col.aggregate([
{
$sort: { "_id.type": -1 }
},
{
$group:{
_id: "$_id.month",
value: { $first: "$value" }
}
},
{
$sort: { _id: 1 }
}
])
Prints:
{ "_id" : 1, "value" : 50 }
{ "_id" : 2, "value" : 40 }
{ "_id" : 3, "value" : 33.532 }
{ "_id" : 4, "value" : 11.244 }
So sorting by type is considered as prioritizing here since we know that lexically exact will be before estimate. You can also be more explicit and add extra field called weight (evaluated using $cond) operator and then sort by that weight:
db.col.aggregate([
{
$addFields: {
weight: { $cond: [ { $eq: [ "$_id.type", "exact" ] }, 2, 1 ] }
}
},
{
$sort: { "weight": -1 }
},
{
$group:{
_id: "$_id.month",
value: { $first: "$value" }
}
},
{
$sort: { _id: 1 }
}
])

How do I compute the difference of two queries?

I have a MongoDB collection that contains a set of documents. Each document has an ISODate date and an integer id (not _id). id: X is said to exist for date: D if there is a document in the collection with field values { id: X, date: D }. So, for example:
{ id: 1, date: 1/1/2000 }
{ id: 1, date: 1/2/2000 }
{ id: 1, date: 1/3/2000 }
{ id: 1, date: 1/4/2000 }
{ id: 2, date: 1/2/2000 }
{ id: 2, date: 1/3/2000 }
{ id: 3, date: 1/3/2000 }
I would like to track ids over time as they are created and destroyed day-to-day. Using the above data, over the date range 1/1/2000 to 1/4/2000:
1/1/2000: id 1 is created
1/2/2000: id 2 is created
1/3/2000: id 3 is created
1/4/2000: id 2 is destroyed
1/4/2000: id 3 is destroyed
I think the best way to solve this would be to loop day by day, see what ids exist between today and the next day, and perform a set difference. For example, to get the set of ids created and destroyed on 1/2/2000, I need to perform two set differences between arrays for either day:
var A = [ <ids that exist on 1/1/2000> ];
var B = [ <ids that exist on 1/2/2000> ];
var created_set = set_difference(B, A); // Those in B and not in A
var destroyed_set = set_difference(A, B); // Those in A and not in B
I can use a find() command to get cursors for A and B, but I do not know how to perform the set_difference between two cursors.
My other option was to use an aggregation pipeline, but I cannot think about how to formulate the pipeline in such a way that I can use the $setDifference operator.
As a MongoDB novice, I am sure I'm thinking about the problem the wrong way. Surely this is something that can be done? What am I missing?
db.mystuff.aggregate([
{$group: {_id: '$id', created: {$first: '$date'}, destroyed: {$last: '$date'}}}
])
Suppose you have the following sample collection:
db.collection.insert([
{ id: 1, date: ISODate("2000-01-01") },
{ id: 1, date: ISODate("2000-01-02") },
{ id: 1, date: ISODate("2000-01-03") },
{ id: 1, date: ISODate("2000-01-04") },
{ id: 2, date: ISODate("2000-01-02") },
{ id: 2, date: ISODate("2000-01-03") },
{ id: 3, date: ISODate("2000-01-03") }
]);
The following aggregation will give you some direction towards what you are trying to achieve using the $setDifference operator:
var start = new Date(2000, 0, 1);
var end = new Date(2000, 0, 2)
db.collection.aggregate([
{
"$match":{
"date": {
"$gte": start,
"$lte": end
}
}
},
{
$group: {
_id: "$date",
"A": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", start ] },
"$id",
false
]
}
},
"B": {
"$addToSet": {
"$cond": [
{ "$eq": [ "$date", end ] },
"$id",
false
]
}
}
}
},
{
"$project": {
"A": {
"$setDifference": [ "$A", [false] ]
},
"B": {
"$setDifference": [ "$B", [false] ]
}
}
},
{
"$project": {
"_id": 0,
"date": "$_id",
"created_set": {
"$setDifference": [ "$B", "$A" ]
},
"destroyed_set": {
"$setDifference": [ "$A", "$B" ]
}
}
}
]);
Output:
{
"result" : [
{
"date" : ISODate("2000-01-02T00:00:00.000Z"),
"created_set" : [2, 1],
"destroyed_set" : []
},
{
"date" : ISODate("2000-01-01T00:00:00.000Z"),
"created_set" : [],
"destroyed_set" : [1]
}
],
"ok" : 1
}