Querying the total number of elements in nested arrays - embed documents MongoDB - mongodb

I have documents in my collections like to:
{
_id: 1,
activities: [
{
activity_id: 1,
travel: [
{
point_id: 1,
location: [-76.0,19.1]
},
{
point_id: 2,
location: [-77.0,19.3]
}
]
},
{
activity_id: 2,
travel: [
{
point_id: 3,
location: [-99.3,18.2]
}
]
}
]
},
{
_id: 2,
activities: [
{
activity_id: 3,
travel: [
{
point_id: 4,
location: [-75.0,11.1]
}
]
}
]
}
I can get the total number of activities, as follows:
db.mycollection.aggregate(
{$unwind: "$activities"},
{$project: {count:{$add:1}}},
{$group: {_id: null, number: {$sum: "$count" }}}
)
I get (3 activities):
{ "result" : [ { "_id" : null, "number" : 3 } ], "ok" : 1 }
question: How can I get the total number of elements in all travels?
expected result: 4 elements
these are:
{
point_id: 1,
location: [-76.0,19.1]
},
{
point_id: 2,
location: [-77.0,19.3]
},
{
point_id: 3,
location: [-99.3,18.2]
},
{
point_id: 4,
location: [-75.0,11.1]
}

You can easily transform document by using double $unwind
e.g.
db.collection.aggregate([
{$unwind: "$activities"},
{$unwind: "$activities.travel"},
{$group:{
_id:null,
travel: {$push: {
point_id:"$activities.travel.point_id",
location:"$activities.travel.location"}}
}},
{$project:{_id:0, travel:"$travel"}}
])
This will emit which is very close to your desired output format:
{
"travel" : [
{
"point_id" : 1.0,
"location" : [
-76.0,
19.1
]
},
{
"point_id" : 2.0,
"location" : [
-77.0,
19.3
]
},
{
"point_id" : 3.0,
"location" : [
-99.3,
18.2
]
},
{
"point_id" : 4.0,
"location" : [
-75.0,
11.1
]
}
]
}
Update:
If you just want to know total number of travel documents in whole collection,
try:
db.collection.aggregate([
{$unwind: "$activities"},
{$unwind: "$activities.travel"},
{$group: {_id:0, total:{$sum:1}}}
])
It will print:
{
"_id" : NumberInt(0),
"total" : NumberInt(4)
}
Update 2:
OP wants to filter documents based on some property in aggregation framework. Here is a way to do so:
db.collection.aggregate([
{$unwind: "$activities"},
{$match:{"activities.activity_id":1}},
{$unwind: "$activities.travel"},
{$group: {_id:0, total:{$sum:1}}}
])
It will print (based on sample document):
{ "_id" : 0, "total" : 2 }

Related

How can i count total documents and also grouped counts simultanously in mongodb aggregation?

I have a dataset in mongodb collection named visitorsSession like
{ip : 192.2.1.1,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.3.1.8,country : 'UK', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.5.1.4,country : 'UK', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.8.1.7,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'},
{ip : 192.1.1.3,country : 'US', type : 'Visitors',date : '2019-12-15T00:00:00.359Z'}
I am using this mongodb aggregation
[{$match: {
nsp : "/hrm.sbtjapan.com",
creationDate : {
$gte: "2019-12-15T00:00:00.359Z",
$lte: "2019-12-20T23:00:00.359Z"
},
type : "Visitors"
}}, {$group: {
_id : "$country",
totalSessions : {
$sum: 1
}
}}, {$project: {
_id : 0,
country : "$_id",
totalSessions : 1
}}, {$sort: {
country: -1
}}]
using above aggregation i am getting results like this
[{country : 'US',totalSessions : 3},{country : 'UK',totalSessions : 2}]
But i also total visitors also along with result like totalVisitors : 5
How can i do this in mongodb aggregation ?
You can use $facet aggregation stage to calculate total visitors as well as visitors by country in a single pass:
db.visitorsSession.aggregate( [
{
$match: {
nsp : "/hrm.sbtjapan.com",
creationDate : {
$gte: "2019-12-15T00:00:00.359Z",
$lte: "2019-12-20T23:00:00.359Z"
},
type : "Visitors"
}
},
{
$facet: {
totalVisitors: [
{
$count: "count"
}
],
countrySessions: [
{
$group: {
_id : "$country",
sessions : { $sum: 1 }
}
},
{
$project: {
country: "$_id",
_id: 0,
sessions: 1
}
}
],
}
},
{
$addFields: {
totalVisitors: { $arrayElemAt: [ "$totalVisitors.count" , 0 ] },
}
}
] )
The output:
{
"totalVisitors" : 5,
"countrySessions" : [
{
"sessions" : 2,
"country" : "UK"
},
{
"sessions" : 3,
"country" : "US"
}
]
}
You could be better off with two queries to do this.
To save the two db round trips following aggregation can be used which IMO is kinda verbose (and might be little expensive if documents are very large) to just count the documents.
Idea: Is to have a $group at the top to count documents and preserve the original documents using $push and $$ROOT. And then before other matches/filter ops $unwind the created array of original docs.
db.collection.aggregate([
{
$group: {
_id: null,
docsCount: {
$sum: 1
},
originals: {
$push: "$$ROOT"
}
}
},
{
$unwind: "$originals"
},
{ $match: "..." }, //and other stages on `originals` which contains the source documents
{
$group: {
_id: "$originals.country",
totalSessions: {
$sum: 1
},
totalVisitors: {
$first: "$docsCount"
}
}
}
]);
Sample O/P: Playground Link
[
{
"_id": "UK",
"totalSessions": 2,
"totalVisitors": 5
},
{
"_id": "US",
"totalSessions": 3,
"totalVisitors": 5
}
]

Group different field by quarter

I've got a aggregation :
{
$group: {
_id: "$_id",
cuid: {$first: "$cuid"},
uniqueConnexion: {
$addToSet: "$uniqueConnexion"
},
uniqueFundraisings: {
$addToSet: "$uniqueFundraisings"
}
}
},
that result with :
{
"cuid" : "cjcqe7qdo00nl0ltitkxdw8r6",
"uniqueConnexion" : [
"09.2019",
"06.2019",
"07.2019",
"08.2019",
"05.2019"
],
"uniqueFundraisings" : [
"06.2019",
"02.2019",
"01.2019",
"03.2019",
"09.2018",
"10.2018"
],
}
And now I'm want to group the uniquerConnexion and uniqueFundraisings fields to a new field (name uniqueAction) and convert them to a quarter format.
So an output like this :
{
"cuid" : "cjcqe7qdo00nl0ltitkxdw8r6",
"uniqueAction" : [
"Q4-2018",
"Q1-2019",
"Q2-2019",
"Q3-2014",
],
}
The previous answer shows the power of $setUnion operating on two lists. I have taken that and expanded a little more to get the OP target state. Given an input that more clearly shows some quarterly grouping (hint!):
var r =
{
"cuid" : "cjcqe7qdo00nl0ltitkxdw8r6",
"uniqueConnexion" : [
"01.2018",
"02.2018",
"08.2018",
"09.2018",
"10.2018",
"11.2018"
],
"uniqueFundraisings" : [
"01.2018",
"02.2018",
"05.2018",
"06.2018",
"12.2018"
],
};
this agg:
db.foo.aggregate([
// Unique-ify the two lists:
{ $project: {
cuid:1,
X: { $setUnion: [ "$uniqueConnexion", "$uniqueFundraisings" ] }
}}
// Now need to get to quarters....
// The input date is "MM.YYYY". Need to turn it into "Qn-YYYY":
,{ $project: {
X: {$map: {
input: "$X",
as: "z",
in: {$let: {
vars: { q: {$toInt: {$substr: ["$$z",0,2] }}},
in: {$concat: [{$cond: [
{$lte: ["$$q", 3]}, "Q1", {$cond: [
{$lte: ["$$q", 6]}, "Q2", {$cond: [
{$lte: ["$$q", 9]}, "Q3", "Q4"] }
]}
]} ,
"-", {$substr:["$$z",3,4]},
]}
}}}}}}
,{ $unwind: "$X"}
,{ $group: {_id: "$X", n: {$sum:1} }}
]);
produces this output. Yes, the OP was not looking for the count of things appearing in each quarter but very often that quickly follows on the heels of the original ask.
{ "_id" : "Q4-2018", "n" : 3 }
{ "_id" : "Q3-2018", "n" : 2 }
{ "_id" : "Q2-2018", "n" : 2 }
{ "_id" : "Q1-2018", "n" : 2 }
i think this will help you
{ $project: {
cuid:1,
uniqueAction: { $setUnion: [ "$uniqueConnexio", "$uniqueAction" ] }, _id: 0
}
}

Mongodb aggregate ifNull against array elements

I have the following dataset:
{
patientId: 228,
medication: {
atHome : [
{
"drug" : "tylenol",
"start" : "3",
"stop" : "7"
},
{
"drug" : "advil",
"start" : "0",
"stop" : "2"
},
{
"drug" : "vitaminK",
"start" : "0",
"stop" : "11"
}
],
}
}
When I execute the following aggregate everything looks great.
db.test01.aggregate(
[
{$match: {patientId: 228}},
{$project: {
patientId: 1,
"medication.atHome.drug": 1
}
},
]);
Results (Exactly what I wanted):
{
"_id" : ObjectId("5a57b7d17af6772ebf647939"),
"patientId" : NumberInt(228),
"medication" : {
"atHome" : [
{"drug" : "tylenol"},
{"drug" : "advil"},
{"drug" : "vitaminK"}
]}
}
We then wanted to add ifNull to change nulls to a default value, but this bungled the results.
db.test01.aggregate(
[
{$match: {patientId: 228}},
{$project: {
patientId: {$ifNull: ["$patientId", NumberInt(-1)]},
"medication.atHome.drug": {$ifNull: ["$medication.atHome.drug", "Unknown"]}
}
},
]);
Results from ifNull (Not what I was hoping for):
{
"_id" : ObjectId("5a57b7d17af6772ebf647939"),
"patientId" : NumberInt(228),
"medication" : {
"atHome" : [
{"drug" : ["tylenol", "advil", "vitaminK"]},
{"drug" : ["tylenol", "advil", "vitaminK"]},
{"drug" : ["tylenol", "advil", "vitaminK"]},
]}
}
What am I missing or not understanding?
To set attributes of documents that are elements of an array to default values you need to $unwind the array and then to group everything up after you check the attributes for null. Here is the query:
db.test01.aggregate([
// unwind to evaluete the array elements
{$unwind: "$medication.atHome"},
{$project: {
patientId: {$ifNull: ["$patientId", -1]},
"medication.atHome.drug": {$ifNull: ["$medication.atHome.drug", "Unknown"]}
}
},
// group to put atHome documents to an array again
{$group: {
_id: {_id: "$_id", patientId: "$patientId"},
"atHome": {$push: "$medication.atHome" }
}
},
// project to get a document of required format
{$project: {
_id: "$_id._id",
patientId: "$_id.patientId",
"medication.atHome": "$atHome"
}
}
])
UPDATE:
There is another more neat query to achieve the same. It uses the map operator to evaluate each array element thus does not require unwinding.
db.test01.aggregate([
{$project:
{
patientId: {$ifNull: ["$patientId", -1]},
"medication.atHome": {
$map: {
input: "$medication.atHome",
as: "e",
in: { $cond: {
if: {$eq: ["$$e.drug", null]},
then: {drug: "Unknown"},
else: {drug: "$$e.drug"}
}
}
}
}
}
}
])

MongoDB: Project to array item with minimum value of field

Suppose my collection consists of items that looks like this:
{
"items" : [
{
"item_id": 1,
"item_field": 10
},
{
"item_id": 2,
"item_field": 15
},
{
"item_id": 3,
"item_field": 3
},
]
}
Can I somehow select the entry of items with the lowest value of item_field, in this case the one with item_id 3?
I'm ok with using the aggregation framework. Bonus point if you can give me the code for the C# driver.
You can use $reduce expression in the following way.
The below query will set the initialValue to the first element of $items.item_field and followed by $lt comparison on the item_field and if true set $$this to $$value, if false keep the previous value and $reduce all the values to find the minimum element and $project to output min item.
db.collection.aggregate([
{
$project: {
items: {
$reduce: {
input: "$items",
initialValue:{
item_field:{
$let: {
vars: { obj: { $arrayElemAt: ["$items", 0] } },
in: "$$obj.item_field"
}
}
},
in: {
$cond: [{ $lt: ["$$this.item_field", "$$value.item_field"] }, "$$this", "$$value" ]
}
}
}
}
}
])
You can use $unwind to seperate items entries.
Then $sort by item_field asc and then $group.
db.coll.find().pretty()
{
"_id" : ObjectId("58edec875748bae2cc391722"),
"items" : [
{
"item_id" : 1,
"item_field" : 10
},
{
"item_id" : 2,
"item_field" : 15
},
{
"item_id" : 3,
"item_field" : 3
}
]
}
db.coll.aggregate([
{$unwind: {path: '$items', includeArrayIndex: 'index'}},
{$sort: { 'items.item_field': 1}},
{$group: {_id: '$_id', item: {$first: '$items'}}}
])
{ "_id" : ObjectId("58edec875748bae2cc391722"), "item" : { "item_id" : 3, "item_field" : 3 } }
We can get expected result using following query
db.testing.aggregate([{$unwind:"$items"}, {$sort: { 'items.item_field': 1}},{$group: {_id: "$_id", minItem: {$first: '$items'}}}])
Result is
{ "_id" : ObjectId("58edf28c73fed29f4b741731"), "minItem" : { "item_id" : 3, "item_field" : 3 } }
{ "_id" : ObjectId("58edec3373fed29f4b741730"), "minItem" : { "item_id" : 3, "item_field" : 3 } }

find documents having a specific count of matches array

I've searched high and low but not been able to find what i'm looking for so apologies if this has already been asked.
Consider the following documents
{
_id: 1,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "B"
},
{
category: "C"
}]
},
{
_id: 2,
items: [
{
category: "A"
},
{
category: "B"
}]
},
{
_id: 3,
items: [
{
category: "A"
},
{
category: "A"
},
{
category: "A"
}]
}
I'd like to be able to find those documents which have more than 1 category "A" item in the items array. So this should find documents 1 and 3.
Is this possible?
Using aggregation
> db.spam.aggregate([
{$unwind: "$items"},
{$match: {"items.category" :"A"}},
{$group: {
_id: "$_id",
item: {$push: "$items.category"}, count: {$sum: 1}}
},
{$match: {count: {$gt: 1}}}
])
Output
{ "_id" : 3, "item" : [ "A", "A", "A" ], "count" : 3 }
{ "_id" : 1, "item" : [ "A", "A" ], "count" : 2 }