Compare integers stored as Strings in Mongodb - mongodb

In the below collection, column "qty" holds the integer values but the datatype is string.
I want to compare the "qty" field with an integer in the aggregate and "warehouse" field with a string "A". ("qty" > 2 and "warehouse" = "A")
[Can't change the datatype in the collection to integer as huge dependency is present]
Edit : Need to retrieve all the columns and all the documents matching the criteria.
Query : getting improper results
db.runCommand(
{
aggregate: "products", pipeline: [
{
$match: {
instock: {
$elemMatch: {
warehouse: "A",
qty: { $gt: "2" }
}
}
}
},
{ $project: { _id: 0 } }],
cursor: { batchSize: 200 }
});
Result : not getting documents where item = journal though it satisfies the conditions
/* 1 */
{
"item" : "paper",
"instock" : [
{
"warehouse" : "A",
"qty" : "60"
},
{
"warehouse" : "B",
"qty" : "15"
}
]
},
/* 2 */
{
"item" : "planner",
"instock" : [
{
"warehouse" : "A",
"qty" : "22"
},
{
"warehouse" : "B",
"qty" : "5"
}
]
}
Products Collection
[
{
"item": "journal",
"instock": [
{
"warehouse": "A",
"qty": "11"
},
{
"warehouse": "C",
"qty": "15"
}
]
},
{
"item": "paper",
"instock": [
{
"warehouse": "A",
"qty": "60"
},
{
"warehouse": "B",
"qty": "15"
}
]
},
{
"item": "planner",
"instock": [
{
"warehouse": "A",
"qty": "22"
},
{
"warehouse": "B",
"qty": "5"
}
]
}
]
Getting improper results as greater than operator in this case is working lexicographically but it should work like integers. Though I tried converting that to double but I am getting no results.
Query with $convert to double : no result
db.runCommand(
{
aggregate: "products", pipeline: [
//{ $match: { "item": { $in: ["planner", "paper","journal"] } } },
{
$match: {
instock: {
$elemMatch: {
warehouse: "A",
qty: {
$gt: [
{$convert:{ input: "$qty", to: "double" }}, 5]
}
}
}
}
},
{ $project: { _id: 0 } }],
cursor: { batchSize: 200 }
});

Try this:
db.products.aggregate([
{
$unwind: "$instock"
},
{
$match: {
$expr: {
$and: [
{
$eq: [
"$instock.warehouse",
"A"
]
},
{
$gt: [
{
$toInt: "$instock.qty"
},
2
]
}
]
}
}
},
{
$group: {
_id: "$_id",
item: {
$first: "$item"
},
instock: {
$push: "$instock"
}
}
},
{
$project: {
_id: 0
}
}
])
MongoPlayground

Try this, it uses $filter to retain objects has criteria :
db.runCommand(
{
aggregate: "products", pipeline: [
{ $match: { 'instock.warehouse': 'A' } },
{
$addFields: {
instockCheck: {
$filter: {
input: '$instock', as: 'each', cond: {
$and: [{ $gt: [{ $toInt: '$$each.qty' }, 2] },
{ $eq: ['$$each.warehouse', 'A'] }]
}
}
}
}
}, { $match: { instockCheck: { $gt: [] } } }, { $project: { instockCheck: 0, _id: 0 } }],
cursor: { batchSize: 200 }
});
Test : MongoDB-Playground

Related

Grouping into array in MongoDB

I have MongoDB collection with below documents:
[
{
"productType":"Bike",
"company":"yamaha",
"model":"y1"
},
{
"productType":"Bike",
"company":"bajaj",
"model":"b1"
},
{
"productType":"Bike",
"company":"yamaha",
"model":"y1"
},
{
"productType":"Car",
"company":"Maruti",
"model":"m1"
},
{
"productType":"Bike",
"company":"yamaha",
"model":"y2"
},
{
"productType":"Car",
"company":"Suzuki",
"model":"s1"
}
]
I want my output to be like :
{
"productType": [
{
"name": "Bike",
"count": 4,
"companies": [
{
"name": "Yamaha",
"count": 3,
"models": [
{
"name": "y1",
"count": 2
},
{
"name": "y2",
"count": 1
}
]
},
{
"name": "Bajaj",
"count": 1,
"models": [
{
"name": "b1",
"count": 1
}
]
}
]
},
{
"name": "Car",
"count": 2,
"companies": [
{
"name": "Maruti",
"count": 1,
"models": [
{
"name": "m1",
"count": 1
}
]
},
{
"name": "Suzuki",
"count": 1,
"models": [
{
"name": "s1",
"count": 1
}
]
}
]
}
]
}
I am not able to understand how to create arrays inside existing array using $push. I know we can create an array using $push but how to create array of array with it ?
In future, I might want to add "metaData" field also along with name and count.
You have to run multiple $group stages, one for each level:
db.collection.aggregate([
{
$group: {
_id: { company: "$company", productType: "$productType", model: "$model" },
count: { $sum: 1 }
}
},
{
$group: {
_id: { productType: "$_id.productType", company: "$_id.company" },
models: { $push: { name: "$_id.model", count: "$count" } },
count: { $sum: "$count" }
}
},
{
$group: {
_id: "$_id.productType",
companies: { $push: { company: "$_id.company", models: "$models", count: "$count" } },
count: { $sum: "$count" }
}
},
{ $set: { name: "$_id", _id: "$$REMOVE" } },
{
$group: {
_id: null,
productType: { $push: "$$ROOT" }
}
}
])
Mongo Playground
Try this:
db.testCollection.aggregate([
{
$group: {
_id: {
name: "$productType",
company: "$company",
model: "$model"
},
count: { $sum: 1 }
}
},
{
$group: {
_id: {
name: "$_id.name",
company: "$_id.company"
},
count: { $sum: "$count" },
models: {
$push: {
name: "$_id.model",
count: "$count"
}
}
}
},
{
$group: {
_id: { name: "$_id.name" },
count: { $sum: "$count" },
companies: {
$push: {
name: "$_id.company",
count: "$count",
models: "$models"
}
}
}
},
{
$group: {
_id: null,
productType: {
$push: {
name: "$_id.name",
count: "$count",
companies: "$companies"
}
}
}
},
{
$project: { _id: 0 }
}
]);
Output:
{
"productType" : [
{
"name" : "Car",
"count" : 2,
"companies" : [
{
"name" : "Suzuki",
"count" : 1,
"models" : [
{
"name" : "s1",
"count" : 1
}
]
},
{
"name" : "Maruti",
"count" : 1,
"models" : [
{
"name" : "m1",
"count" : 1
}
]
}
]
},
{
"name" : "Bike",
"count" : 4,
"companies" : [
{
"name" : "yamaha",
"count" : 3,
"models" : [
{
"name" : "y2",
"count" : 1
},
{
"name" : "y1",
"count" : 2
}
]
},
{
"name" : "bajaj",
"count" : 1,
"models" : [
{
"name" : "b1",
"count" : 1
}
]
}
]
}
]
}

MongoDB/Mongoose: append related records to each aggreation result

Given the following Mongo collection called "members"
{
{name: "Joe", hobby: "Food"}, {name: "Lyn", hobby: "Food"},
{name: "Rex", hobby: "Play"}, {name: "Rex", hobby: "Shop"},...
}
I have an aggregation query that returns a paged set of records along with metadata for the total records found:
db.members.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [{ $skip: 0 }, { $limit: 4 }],
},
},
{
$unwind: '$pipe1',
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2',
},
},
])
This gives me:
{count: 454, results: [<First 4 records here>]}
I am now trying to add to each record, an array of all member names that have the same hobby. So for the collection above, something like:
{
count: 454,
results: [
{name: "Joe", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Lyn", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Rex", hobby: "Play", fanClub: ["Rex"]},
{name: "Rex", hobby: "Shop", fanClub: ["Rex", "Rita"]}
]
}
I can't figure out how to run the follow up query within the aggregate. I've tried:
db.members.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [
{ $skip: 0 },
{ $limit: 2 },
{
$lookup: {
from: 'members',
pipeline: [{ $match: { hobby: '$hobby' } }],
as: 'fanClub',
},
},
],
},
},
{
$unwind: '$pipe1',
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2',
},
},
])
Alas, the fanClub array is always empty.
Update 1
If I hardcode the hobby, for instance replace
{ $match: { hobby: '$hobby' }
with
{ $match: { hobby: 'Food' }
Then I do get results and all the fanClub arrays contain the results for Joe, Lyn and Alfred. So I must not be referring to the value within the pipeline correctly
Please try this :
db.membersHobby.aggregate([
{
$facet: {
pipe1: [{ $count: 'count' }],
pipe2: [{
$lookup:
{
from: "membersHobby",
let: { hobby: "$hobby" },
pipeline: [
{
$match:
{ $expr: { $eq: ["$hobby", "$$hobby"] } }
},
{ $project: { name: 1, _id: 0 } }
],
as: "fanClub"
}
}, { $skip: 0 }, { $limit: 4 }]
}
},
{
$unwind: '$pipe1'
},
{
$project: {
count: '$pipe1.count',
results: '$pipe2'
}
}
])
Result :
/* 1 */
{
"count" : 4,
"results" : [
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4a"),
"name" : "Joe",
"hobby" : "Food",
"fanClub" : [
{
"name" : "Joe"
},
{
"name" : "Lyn"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4b"),
"name" : "Lyn",
"hobby" : "Food",
"fanClub" : [
{
"name" : "Joe"
},
{
"name" : "Lyn"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4c"),
"name" : "Rex",
"hobby" : "Play",
"fanClub" : [
{
"name" : "Rex"
}
]
},
{
"_id" : ObjectId("5e20a63ed3c98f2a7100fd4d"),
"name" : "Rex",
"hobby" : "Shop",
"fanClub" : [
{
"name" : "Rex"
}
]
}
]
}
If #srinivasy's answer meets your requierements, please grant my points him :)
If you want to get such structure:
{
count: 454,
results: [
{name: "Joe", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Lyn", hobby: "Food", fanClub: ["Joe", "Lyn", "Alfred"]},
{name: "Rex", hobby: "Play", fanClub: ["Rex"]},
{name: "Rex", hobby: "Shop", fanClub: ["Rex", "Rita"]}
]
}
Use this query ($reduce is used to return single value, in you case fanClub as array):
db.members.aggregate([
{
$facet: {
pipe1: [
{
$count: "count"
}
],
pipe2: [
{
$skip: 0
},
{
$limit: 4
},
{
$lookup: {
from: "members",
let: {
hobby: "$hobby"
},
pipeline: [
{
$match: {
$expr: {
$eq: [
"$hobby",
"$$hobby"
]
}
}
}
],
as: "fanClub"
}
}
]
}
},
{
$unwind: "$pipe1"
},
{
$project: {
count: "$pipe1.count",
results: {
$map: {
input: "$pipe2",
as: "pipe2",
in: {
_id: "$$pipe2._id",
hobby: "$$pipe2.hobby",
name: "$$pipe2.name",
fanClub: {
$reduce: {
input: "$$pipe2.fanClub",
initialValue: [],
in: {
$concatArrays: [
"$$value",
[
"$$this.name"
]
]
}
}
}
}
}
}
}
}
])
MongoPlayground

Compare 2 count aggregations

I have a collection in MongoDB that looks something like the following:
{ "_id" : 1, "type" : "start", userid: "101", placementid: 1 }
{ "_id" : 2, "type" : "start", userid: "101", placementid: 2 }
{ "_id" : 3, "type" : "start", userid: "101", placementid: 3 }
{ "_id" : 4, "type" : "end", userid: "101", placementid: 1 }
{ "_id" : 5, "type" : "end", userid: "101", placementid: 2 }
and I want to group results by userid then placementid and then count the types of "start" and "end", but only when the two counts are different. In this particular example I would want to get placementid: 3 because when grouped and counted this is the only case where the counts don't match.
I've written a query that gets the 2 counts and the grouping but I can't do the filtering when counts don't match. This is my query:
db.getCollection('mycollection').aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$match: {
countStart: {$ne: "$countEnd"}
}
}
])
It seems like I'm using the match aggregation incorrectly because I'm seeing results where countStart and countEnd are the same.
{ "_id" : {"userid" : "101", "placementid" : "1"}, "countStart" : 1.0, "countEnd" : 1.0 }
{ "_id" : {"userid" : "101", "placementid" : "2"}, "countStart" : 1.0, "countEnd" : 1.0 }
{ "_id" : {"userid" : "101", "placementid" : "3"}, "countStart" : 1.0, "countEnd" : 0 }
Can anybody point into the right direction please?
To compare two fields inside $match stage you need $expr which is available in MongoDB 3.6:
db.myCollection.aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$match: {
$expr: { $ne: [ "$countStart", "$countEnd" ] }
}
}
])
If you're using older version of MongoDB you can use $redact:
db.myCollection.aggregate([
{
$project: {
userid: 1,
placementid: 1,
isStart: {
$cond: [ { $eq: ["$type", "start"] }, 1, 0]
},
isEnd: {
$cond: [ { $eq: ["$type", "end"] }, 1, 0]
}
}
},
{
$group: {
_id: { userid:"$userid", placementid:"$placementid" },
countStart:{ $sum: "$isStart" },
countEnd: { $sum: "$isEnd" }
}
},
{
$redact: {
$cond: { if: { $ne: [ "$countStart", "$countEnd" ] }, then: "$$KEEP", else: "$$PRUNE" }
}
}
])
You run do the following pipeline to get this - no need to use $expr or $redact or anything special really:
db.mycollection.aggregate({
$group: {
_id: {
"userid": "$userid",
"placementid": "$placementid"
},
"sum": {
$sum: {
$cond: {
if: { $eq: [ "$type", "start" ] },
then: 1, // +1 for start
else: -1 // -1 for anything else
}
}
}
}
}, {
$match: {
"sum": { $ne: 0 } // only return the non matching-up ones
}
})

Aggregate Fields together

I have the following structure as an input from which data needs to be aggregated:
I need to aggregate the data such that I end up with the following structure:
start: A {
tripdetails: [{
destination: B [{
duration: 10,
type: male
},
duration: 12,
type: female
},
duration: 9,
type: female
}]
]}
}
Basically I need to group "type" and "duration" together under the same destination.
I came up with the following query, but this results in a a single field for "type" for each "destination", but not for every "duration".
db.test.aggregate(
{
$group: {
_id: {"StationID": "$start", "EndStationID": "$destination"},
durations: {$addToSet: "$duration" },
usertypes: {$addToSet: "$type" }
}
},
{
$group: {
_id: "$_id.StationID",
Tripcount_out: {$sum: "durations"},
Trips_out: { $addToSet: { EndStationID: "$_id.EndStationID", Tripduration: "$durations", Usertype: "$usertypes"} }
}
}
)
My question is how I can achieve the structure described above.
You could try running the following aggregate pipeline:
db.test.aggregate([
{
"$group": {
"_id": { "StationID": "$start", "EndStationID": "$destination" },
"details": {
"$push": {
"duration": "$duration",
"type": "$type"
}
}
}
},
{
"$group": {
"_id": "$_id.StationID",
"tripdetails": {
"$push": {
"destination": "$_id.EndStationID",
"trips": "$details"
}
}
}
}
])
which yields:
{
"_id" : "A",
"tripdetails" : [
{
"destination" : "B",
"trips" : [
{
"duration" : 10,
"type" : "male"
},
{
"duration" : 9,
"type" : "female"
},
{
"duration" : 12,
"type" : "female"
}
]
}
]
}

Use $size with $sort in array and sub array

Here's the structure part of my collection:
_id: ObjectId("W"),
names: [
{
number: 1,
subnames: [ { id: "X", day: 1 }, { id: "Y", day: 10 }, { id: "Z", day: 2 } ],
list: ["A","B","C"],
day: 1
},
{
number: 2,
day: 5
},
{
number: 3,
subnames: [ { id: "X", day: 8 }, { id: "Z", day: 5 } ],
list: ["A","C"],
day: 2
},
...
],
...
I use this request:
db.publication.aggregate( [ { $match: { _id: ObjectId("W") } }, { $group: { _id: "$_id", SizeName: { $first: { $size: { $ifNull: [ "$names", [] ] } } }, names: { $first: "$names" } } }, { $unwind: "$names" }, { $sort: { "names.day": 1 } }, { $group: { _id: "$_id", SzNames: { $sum: 1 }, names: { $push: { number: "$names.number", subnames: "$names.subnames", list: "$names.list", SizeList: { $size: { $ifNull: [ "$names.list", [] ] } } } } } } ] );
but I would now use $sort for my names array AND my subnames array to obtain this result (subnames may not exist) :
_id: ObjectId("W"),
names: [
{
number: 2,
SizeList: 0,
day: 5
},
{
number: 3,
subnames: [ { id: "Z", day: 5 }, { id: "X", day: 8 } ],
list: ["A","C"],
SizeList: 2,
day: 2
},
{
number: 1,
subnames: [ { id: "X", day: 1 }, { id: "Z", day: 2 }, { id: "Y", day: 10 } ],
list: ["A","B","C"],
SizeList: 3,
day: 1
}
...
],
...
Can you help me ?
You can do this, but with great difficulty. I for one would gladly vote for an inline version of $sort along the lines of the $map operator. That would makes things so much easier.
For now though you need to de-construct and re-build the arrays after sorting. And you have to be very careful about this. Hence make false arrays with a single entry before processing $unwind:
db.publication.aggregate([
{ "$project": {
"SizeNames": {
"$size": {
"$ifNull": [ "$names", [] ]
}
},
"names": { "$ifNull": [{ "$map": {
"input": "$names",
"as": "el",
"in": {
"SizeList": {
"$size": {
"$ifNull": [ "$$el.list", [] ]
}
},
"SizeSubnames": {
"$size": {
"$ifNull": [ "$$el.subnames", [] ]
}
},
"number": "$$el.number",
"day": "$$el.day",
"subnames": { "$ifNull": [ "$$el.subnames", [0] ] },
"list": "$$el.list"
}
}}, [0] ] }
}},
{ "$unwind": "$names" },
{ "$unwind": "$names.subnames" },
{ "$sort": { "_id": 1, "names.subnames.day": 1 } },
{ "$group": {
"_id": {
"_id": "$_id",
"SizeNames": "$SizeNames",
"names": {
"SizeList": "$names.SizeList",
"SizeSubnames": "$names.SizeSubnames",
"number": "$names.number",
"list": "$names.list",
"day": "$names.day"
}
},
"subnames": { "$push": "$names.subnames" }
}},
{ "$sort": { "_id._id": 1, "_id.names.day": 1 } },
{ "$group": {
"_id": "$_id._id",
"SizeNames": { "$first": "$_id.SizeNames" },
"names": {
"$push": { "$cond": [
{ "$ne": [ "$_id.names.SizeSubnames", 0 ] },
{
"number": "$_id.names.number",
"subnames": "$subnames",
"list": "$_id.names.list",
"SizeList": "$_id.names.SizeList",
"day": "$_id.names.day"
},
{
"number": "$_id.names.number",
"list": "$_id.names.list",
"SizeList": "$_id.names.SizeList",
"day": "$_id.names.day"
}
]}
}
}},
{ "$project": {
"SizeNames": 1,
"names": {
"$cond": [
{ "$ne": [ "$SizeNames", 0 ] },
"$names",
[]
]
}
}}
])
You can kind of "hide away" the original empty array from the inner document as shown, but it's really difficult to remove all presence of the outer "names" array without pulling a similar conditional array "push" technique, and that really isn't a practical approach.
If all of this is just about sorting array elements in individual documents though, the aggregation framework should not be the tool to do this. It can be done as shown, but per document this is much easier to do in client side code.
Output:
{
"_id" : ObjectId("54b5cff8102f292553ce9bb5"),
"SizeNames" : 3,
"names" : [
{
"number" : 1,
"subnames" : [
{
"id" : "X",
"day" : 1
},
{
"id" : "Z",
"day" : 2
},
{
"id" : "Y",
"day" : 10
}
],
"list" : [
"A",
"B",
"C"
],
"SizeList" : 3,
"day" : 1
},
{
"number" : 3,
"subnames" : [
{
"id" : "Z",
"day" : 5
},
{
"id" : "X",
"day" : 8
}
],
"list" : [
"A",
"C"
],
"SizeList" : 2,
"day" : 2
},
{
"number" : 2,
"SizeList" : 0,
"day" : 5
}
]
}