Is it possible to aggregate such different cases in a single query? - arrays

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
}
]
}
]

Related

Remove 3 similar items from an array using mongodb

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"
}
}
])

How to divide documents values into same document, however key name of the divisor is same in MongoDB

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

How to group an array of subdocuments by multiple fields?

I have spent the better part of the day on this and am now out of ideas. Here is my collection:
[
{
"_id": "ID_XXX",
"logs": [
{
"lead_id": 123,
"list_id": "list_44",
"order_id": "order_1"
},
{
"lead_id": 124,
"list_id": "list_44",
"order_id": "order_2"
}
]
},
{
"_id": "ID_YYY",
"logs": [
{
"lead_id": 125,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 126,
"list_id": "list_44",
"order_id": "order_2"
},
{
"lead_id": 127,
"list_id": "list_44",
"order_id": "order_3"
},
{
"lead_id": 128,
"list_id": "list_66",
"order_id": "order_3"
}
]
}
]
I'm just trying to get the counts for list_id and order_id while preserving the _id of the document they are in. Here is my desired output:
[
{
"_id": "ID_XXX",
"counts": [
{
"lists": {"list_44": 2},
},
{
"orders": {"order_1": 1, "order_2": 1}
}
]
},
{
"_id": "ID_YYY",
"counts": [
{
"lists": {"list_44": 3, "list_66": 1},
},
{
"orders": {"order_2": 2, "order_3": 2}
}
]
}
]
I have tried way too many aggregate variations to list here, but the latest is this:
db.collection.aggregate([
{
$unwind: "$logs"
},
{
$group: {
_id: "$_id",
lists: {
$push: "$logs.list_id"
},
orders: {
$push: "$logs.order_id"
}
}
}
])
Which does not give me what I want. Can anyone point me in the right direction? Here is the playground link: https://mongoplayground.net/p/f-jk7lbSrJ0
$reduce to iterate loop of logs, convert logs object to array in k(key) v(value) format using $objectToArray, $concatArrays with initialValue in $reduce,
$filter above reduce result as input and filter required fields from logs
$unwind deconstruct logs array
db.collection.aggregate([
{
$addFields: {
logs: {
$filter: {
input: {
$reduce: {
input: "$logs",
initialValue: [],
in: { $concatArrays: ["$$value", { $objectToArray: "$$this" }] }
}
},
cond: { $in: ["$$this.k", ["list_id", "order_id"]] }
}
}
}
},
{ $unwind: "$logs" },
$group by _id and logs object and get the total count using $sum
{
$group: {
_id: {
_id: "$_id",
logs: "$logs"
},
counts: { $sum: 1 }
}
},
$group by _id and make lists array if logs.k is list_id and return in k and v format otherwise $$REMOVE, same as for orders make an array of order on the base of order_id
$addFields to convert lists array from k and v format to object format using $arrayToObjectand same as fororders` array
{
$group: {
_id: "$_id._id",
lists: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "list_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
},
orders: {
$push: {
$cond: [
{ $eq: ["$_id.logs.k", "order_id"] },
{
k: "$_id.logs.v",
v: "$counts"
},
"$$REMOVE"
]
}
}
}
},
{
$addFields: {
lists: { $arrayToObject: "$lists" },
orders: { $arrayToObject: "$orders" }
}
}
])
Playground

MongoDB aggregate fill/replace array of ObjectId

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

Combine multiple projections in MongoDB aggregation

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
}
}
}
]

Resources