My MongoDB (v4.2) pipeline consists of multiple projections to subtract, divide, ceil and multiply a value.
Is there a way to combine them without using multiple projections?
My pipeline:
[
{
$project: {
duration: {
$subtract: [ '$updated', '$created' ]
}
}
},
{
$project: {
duration: {
$divide: [ '$duration', precision ]
}
}
},
{
$project: {
duration: {
$ceil: [ '$duration' ]
}
}
},
{
$project: {
duration: {
$multiply: [ '$duration', precision ]
}
}
},
{
$group: {
_id: '$duration',
count: {
$sum: 1
}
}
}
]
Example data:
[
{ created: 1569763367, updated: 1569773367 },
{ created: 1569760000, updated: 1569770000 },
{ created: 1569772415, updated: 1569773519 },
]
It's seems to be possible, but it's not very readable:
[
{
$project: {
duration: {
$multiply: [
{
$ceil: [
{
$divide: [
{
$subtract: [ '$updated', '$created' ]
},
precision
]
}
]
},
precision
]
}
}
},
{
$group: {
_id: '$duration',
count: {
$sum: 1
}
}
}
]
Related
I have a collection in MongoDB with many documents like the ones below:
[{
action: "SEND_ACTION_START",
data: {
stuff: {
delivered: {
a: 1,
b: 2,
c: 3
}
}
}
},
{
action: "SEND_ACTION",
data: {
stuff: {
delivered: [
{0: {
a: 1,
b: 2,
c: 3
}}
]
}
}
},
{
action: "SEND",
data: {
stuff: {
delivered: [
{0: {
a: 1,
b: 2,
c: 3
}},
{1: {
a: 11,
b: 22,
c: 33
}}
],
marker: 0
}
}
}]
Every case in the above array of documents has its own different structure: the first one is easy to query for fields, but having the other two makes the task difficult.
The second document contains an array of delivered with only a single element but the third case can contain many objects inside the array and that's the reason of the marker field which indicates which element on the array should be pulled out.
I would like to retrieve such documents in a single aggregation query (which will reduce the database stress as it is in a production environment), and get something like the document below:
{
a: 1,
b: 2,
c: 3
}
Can it be made in MongoDB from 4.4 version on?
https://mongoplayground.net/p/kZSugkMmogf
Guess, the keys 0 and 1 are redundant.
db.collection.aggregate([
{
$match: {
action: "SEND",
}
},
{
$replaceRoot: {
newRoot: {
$arrayElemAt: [
"$data.stuff.delivered",
{
$toInt: "$data.stuff.marker"
}
]
}
}
}
])
[
{
"0": {
"a": 1,
"b": 2,
"c": 3
}
}
]
https://mongoplayground.net/p/6utBamY5-KW
I'm sure it can be simplified:
db.collection.aggregate([
{
$replaceRoot: {
newRoot: "$data.stuff"
}
},
{
$facet: {
a: [
{
$match: {
$expr: {
$eq: [
{
$type: "$delivered"
},
"object"
]
}
}
},
//
//
{
$replaceRoot: {
newRoot: "$delivered"
}
},
],
///
b: [
{
$match: {
$expr: {
$eq: [
{
$type: "$delivered"
},
"array"
]
}
},
},
//
{
$match: {
marker: {
"$exists": false
}
}
},
//
{
$project: {
delivered: {
$first: "$delivered"
}
}
},
//
{
$project: {
delivered: {
$objectToArray: "$delivered"
}
}
},
//
{
$replaceRoot: {
newRoot: {
$first: "$delivered.v"
}
}
},
],
//
c: [
{
$match: {
marker: {
"$exists": true
}
}
},
{
$replaceRoot: {
newRoot: {
$last: {
$objectToArray: {
$arrayElemAt: [
"$delivered",
{
$toInt: "$marker"
}
]
}
}
}
}
},
//
{
$replaceRoot: {
newRoot: "$v"
}
}
]
}
},
//
{
$project: {
result: {
$concatArrays: [
"$a",
"$b",
"$c"
]
}
}
},
])
[
{
"result": [
{
"a": 41,
"b": 42,
"c": 43
},
{
"a": 31,
"b": 32,
"c": 33
},
{
"a": 11,
"b": 22,
"c": 33
}
]
}
]
I have this document and i need remove only 3 items with name "Test" using only 1 request.
{ "_id" : 1, "items" : ["Test, "Test", "Test", "Test", "Test", "Sword", "Sword]}
It must become { "_id" : 1, "items" : ["Test", "Test", "Sword", "Sword]} after request.
Maybe something like this:
db.collection.aggregate([
{
"$unwind": "$items"
},
{
$group: {
_id: "$items",
cnt: {
$sum: 1
},
it: {
$push: "$items"
},
origid: {
"$first": "$_id"
}
}
},
{
$project: {
_id: "$origid",
items: {
$slice: [
"$it",
0,
2
]
}
}
},
{
$group: {
_id: "$_id",
items: {
$push: "$items"
}
}
},
{
$addFields: {
items: {
"$reduce": {
"input": "$items",
"initialValue": [],
"in": {
"$concatArrays": [
"$$this",
"$$value"
]
}
}
}
}
}
])
Explained:
unwind the items array so you can group after
group by item so you get the repeating values
project with slice to remove more then 2x items in array
group to form back the document but without the >2 repeating values
project with concatArrays to concat arrays of items.
Playground
You can use this aggregation to get the desired result. The query uses the $reduce Aggregate Array Operator to iterate over the items array, and discards the first 3 matching "Test" items.
db.collection.aggregate([
{
$set: {
items: {
$reduce: {
input: "$items",
initialValue: { result: [], count: 0 },
in: {
$cond: [ { $and: [ { $eq: [ "$$this", "Test" ] }, { $lt: [ "$$value.count", 3 ] } ] },
{ result: "$$value.result", count: { $add: [ "$$value.count", 1 ] } },
{ result: { $concatArrays: [ "$$value.result", [ "$$this" ] ] }, count: "$$value.count" }
]
}
}
}
}
},
{
$set: {
items: "$items.result"
}
}
])
In data_start if 0 is followed by >=1 in the array then remove the 0 from the data_start , also remove the element from time_start array accordingly.
{
"data_start": [
1,
1,
0,
0,
1,
1,
0
],
"time_start": [
"2021-09-04T12:18:42Z",
"2021-09-04T14:59:50Z",
"2021-09-04T14:59:59Z",
"2021-09-04T15:00:00Z",
"2021-09-04T15:00:01Z",
"2021-09-04T15:05:00Z",
"2021-09-04T15:05:01Z"
]
}
Output Document will be :
{
"data_start": [
1,
1,
0,
1,
1,
0
],
"time_start": [
"2021-09-04T12:18:42Z",
"2021-09-04T14:59:50Z",
"2021-09-04T14:59:59Z",
"2021-09-04T15:00:01Z",
"2021-09-04T15:05:00Z",
"2021-09-04T15:05:01Z"
]
}
One approach is this one:
db.collection.aggregate([
{
$set: {
data: {
$let: {
vars: {
val: "$data_start",
prev: { $concatArrays: [[null], "$data_start"] } // shift values by one element
},
in: {
$map: {
input: { $range: [0, { $size: "$$val" }] },
as: "idx",
in: {
$cond: {
if: {
$and: [
{ $eq: [{ $arrayElemAt: ["$$val", "$$idx"] }, 0] }, // is 0
{ $eq: [{ $arrayElemAt: ["$$val", "$$idx"] }, { $arrayElemAt: ["$$prev", "$$idx"] }] } // is equal to previous
]
},
then: null,
else: {
data_start: { $arrayElemAt: ["$$val", "$$idx"] },
time_start: { $arrayElemAt: ["$time_start", "$$idx"] }
}
}
}
}
}
}
}
}
},
{
$set: {
data: {
$filter: {
input: "$data",
cond: "$$this"// -> removes null's from array
}
}
}
}
])
Maybe you have to fine-tune the condition and/or reverse the loop, i.e. input: { $range: [{ $size: "$$val" }, 0, -1] }
Mongo playground
This is a little ugly in Mongo but possible, the strategy for us would be to first iterate over the array and calculate the indexes of the items we need to remove, once we have those we will iterate over the two arrays again and update them and finally we remove the temporary calculated values, like so:
Note that to do this purely in Mongo you will need to be on v4.2+ as we need to use pipelined updates if you're using a lesser version then you will have execute this logic in code.
db.collection.updateOne(
{},
[
{
$set: {
tmpField: {
$reduce: {
input: {
$zip: {
"inputs": [
{
$reverseArray: "$data_start"
},
{
$range: [
{
$subtract: [
{
$size: "$data_start"
},
1
]
},
0,
-1
]
}
]
}
},
initialValue: {
prev: null,
indexesToRemove: []
},
in: {
prev: {
$arrayElemAt: [
"$$this",
0
]
},
indexesToRemove: {
$concatArrays: [
"$$value.indexesToRemove",
{
$cond: [
{
$and: [
{
$eq: [
"$$value.prev",
1
]
},
{
$eq: [
{
$arrayElemAt: [
"$$this",
0
]
},
0
]
}
]
},
[
{
$arrayElemAt: [
"$$this",
1
]
}
],
[]
]
}
]
}
}
}
}
}
},
{
$addFields: {
data_start: {
$reduce: {
input: {
$zip: {
"inputs": [
"$data_start",
{
$range: [
0,
{
$size: "$data_start"
}
]
}
]
}
},
initialValue: [],
in: {
$cond: [
{
$in: [
{
$arrayElemAt: [
"$$this",
1
]
},
"$tmpField.indexesToRemove"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
{
$arrayElemAt: [
"$$this",
0
]
}
]
]
}
]
}
}
},
time_start: {
$reduce: {
input: {
$zip: {
"inputs": [
"$time_start",
{
$range: [
0,
{
$size: "$time_start"
}
]
}
]
}
},
initialValue: [],
in: {
$cond: [
{
$in: [
{
$arrayElemAt: [
"$$this",
1
]
},
"$tmpField.indexesToRemove"
]
},
"$$value",
{
$concatArrays: [
"$$value",
[
{
$arrayElemAt: [
"$$this",
0
]
}
]
]
}
]
}
}
}
}
},
{
$unset: "tmpField"
}
])
Mongo Playground
Explanation: want to divide the name: Alex , name: petr value by name: hr value.
name: Alex , name: petr and name: hr are my parameters names.
also want to see the value of name: hr in the output document.
[
{
"name": "Alex",
"value": 65
},
{
"name": "petr",
"value": 8
},
{
"name": "hr",
"value": 20
}
]
Expected Output :
[
{
"name": "Alex/hr",
"value": 3.25
},
{
"name": "petr/hr",
"value": 0.4
},
{
"name": "hr",
"value": 20
}
]
Demo - https://mongoplayground.net/p/38G_Loo8V86
Use $facet
db.collection.aggregate([
{
$facet: {
hrVal: [
{ $match: { name: "hr" } }, // filter
{ $project: { _id: 0, value: 1 } } // take only value
],
allValues: [] // all documents or you can add match pipeline to filter here
}
},
{ $unwind: "$hrVal" }, // break into individual documents - now every document will have hrVal.value
{ $unwind: "$allValues" }, // break into individual documents
{
$set: { //
"allValues": {
"$cond": [
{ $eq: [ "$allValues.name", "hr" ] }, // condition
"$allValues", // true
{ // false
_id: "$allValues._id",
name: { "$concat": [ "$allValues.name", "/hr" ] }, // set name
value: { "$divide": [ "$allValues.value", "$hrVal.value" ] } // divide by hr value
}
]
}
}
},
{ $replaceRoot: { "newRoot": "$allValues" } } // reset to orignal document shape
])
$lookup with same collection and match for name: hr and return single result
$unwind deconstruct hr array
$project to check condition if name is hr then return current name and value of not then concat name using $concat and divide value by $divide
$project to move doc object to root
db.collection.aggregate([
{
$lookup: {
from: "collection",
pipeline: [
{ $match: { name: "hr" } },
{ $limit: 1 }
],
as: "hr"
}
},
{ $unwind: "$hr" },
{
$project: {
doc: {
$cond: [
{ $eq: ["$name", "hr"] },
{
name: "$name",
value: "$value"
},
{
name: { $concat: ["$name","/","$hr.name"] },
value: { $divide: ["$value", "$hr.value"] }
}
]
}
}
},
{
$project: {
name: "$doc.name",
value: "$doc.value"
}
}
])
Playground
Second option without lookup,
$facet to separate both result
$map to iterate loop of result array and check name is hr then concat name and divide value otherwise return same
$unwind deconstruct result array
$project to show fields
db.collection.aggregate([
{
$facet: {
result: [{ $match: {} }],
hr: [{ $match: { name: "hr" } }]
}
},
{
$project: {
result: {
$map: {
input: "$result",
in: {
$cond: [
{ $eq: ["$$this.name", "hr"] },
"$$this",
{
name: { $concat: ["$$this.name", "/", { $first: "$hr.name" }] },
value: { $divide: ["$$this.value", { $first: "$hr.value" }] }
}
]
}
}
}
}
},
{ $unwind: "$result" },
{
$project: {
name: "$result.name",
value: "$result.value"
}
}
])
Playground
I have the following MongoDB structure:
campaign:{
_id: '5e4eee638552043e60e5073b',
name: 'Test campaign',
partners: [
{
_id: '5e4fa9fbbdeb4a5878ac2a35',
options: [Object],
partner: '5e4e9bc2101ecb2e8764190c',
},
{
_id: '5e4ff7c6e54b6d676d97b7a6',
options: [Object],
partner: '5e4ff51c23f44266a8add39e',
}
]
}
Using the following aggregate function, I can lookup the ID of each of the partners and add to a new array:
{
$lookup:
{
from: 'campaigns',
let: { campaign: '$campaign' },
pipeline: [
{ $match: { $expr: { $and: [{ $eq: ['$_id', '$$campaign'] }] } } },
{
$lookup:
{
from: 'partners',
let: { partner: '$partner.partner' },
pipeline: [
{ $match: { $expr: { $in: ['$_id', '$$partner'] } } },
{ $project: { _id: 1, name: 1 } }
],
as: 'partner1'
}
},
{
$project: {
_id: 1, shortName: 1, userInterface: 1, options: 1, check: 1, description: 1, partner: 1, partner1: 1 } }
],
as: 'campaign'
}
}
This is new output:
campaign: {
_id: '5e4eee638552043e60e5073b',
name: 'Test campaign',
partners: [
...
],
partner1: [
{ _id: '5e4e9bc2101ecb2e8764190c', name: '123' },
{ _id: '5e4ff51c23f44266a8add39e', name: '456' }
]
}
But what I want to do is have the _id of each element in partner array be replaced with the full partner object.
Partner object:
partner
{
_id: '5e4e9bc2101ecb2e8764190c',
name: '123',
etc: ...
},
{
_id: '5e4ff51c23f44266a8add39e',
name: '456'
etc: ...
}
This is what the final object should look like:
[
{
"campaign": [
{
"partner": [
{
"_id": "5e4eee638552043e60e5073b",
"options": [
{}
],
"partner": {
"_id": "5e4e9bc2101ecb2e8764190c",
"name": "123"
}
}
]
}
]
}
]
You need to add extra 3 stages:
db.collection.aggregate([
//your $lookup,
{
$unwind: "$campaign"
},
{
$project: {
_id: 1,
"campaign.partner": {
$map: {
input: "$campaign.partner",
as: "partner",
in: {
$let: {
vars: {
partner1: {
$filter: {
input: "$campaign.partner1",
cond: {
$eq: [
"$$partner.partner",
"$$this._id"
]
}
}
}
},
in: {
// we merge partner[i] with partner1[j]
$mergeObjects: [
"$$partner",
{
partner: {
$arrayElemAt: [
"$$partner1",
0
]
}
}
]
}
}
}
}
}
}
},
{
$group: {
_id: "$_id",
campaign: {
$push: "$campaign"
}
}
}
])
MongoPlayground