mongodb: Select the latest entry in an embedded array - mongodb

Given the following data of Items with a price history:
{
item: "Item A",
priceHistory: [
{
date: ISODate("2021-04-01T08:32:45.561Z"),
value: 100
},
{
date: ISODate("2021-04-02T08:32:45.561Z"),
value: 200
},
{
date: ISODate("2021-04-04T08:32:45.561Z"),
value: 400
},
{
date: ISODate("2021-04-03T08:32:45.561Z"),
value: 300
},
]
},{
item: "Item B",
priceHistory: [
{
date: ISODate("2021-04-01T08:32:45.561Z"),
value: 1
}
]
}, ...
Note that the priceHistory field is not sorted.
I want to find the latest price for each item:
{
item: "Item A",
price: 400
},{
item: "Item B",
price: 1
}, ...
Now I'm struggling to select the LATEST entry of the priceHistory
What I tried already
I know that I can use { $unwind: "$priceHistory" } to get a result for each entry in priceHistory.
With $max: "$priceHistory.date" I can find the latest date
I know that since MongoDB 4.4 there is $last to get the last item in an array -> not useful here since items are not in order
But I struggle to bring it all together.
On a side note, maybe the problem lies within the data model itself? Would it make sense to segregate price history into its own collection, and only store the latest price on the item itself?

Demo - https://mongoplayground.net/p/-LQPcTn_-Aj
db.collection.aggregate([
{ $unwind: "$priceHistory" }, // unwind to individual documents
{ $sort: { "priceHistory.date": -1 } }, // sort by priceHistory.date to get max date at the top (descending)
{
$group: {
_id: "$_id", // group by id back and get priceHistory sorted in descending order by date
price: { $first: "$priceHistory.value" }, // get the first price which is for max date record
item: { $first: "$item"}
}
}
])

Related

Sort data based on given id first

Suppose I want to sort the data based on the current city first and then the remaining country data. Is there any way I achieve that in MongoDB?
Example
[
{ id: 2, name: 'sdf' },
{ id: 3, name: 'sfs' },
{ id: 3, name: 'aaa' },
{ id: 1, name: 'dsd' },
];
What I want as an outcome is the data with id 3 at first and the remaining other.
like
[
{ id: 3, name: 'sfs' },
{ id: 3, name: 'aaa' },
{ id: 1, name: 'dsd' },
{ id: 2, name: 'sdf' },
];
It's just a example,
My actual requirement is to sort the data based on certain category first and then the remaining one
It's not possible within mongodb but you could first fetch the documents from the db and then sort them in Javascript (or whatever other language you're using to present the data).
On a side note, having duplicate values in the "id" field is not a good practice and defies the definition of id itself.
There is no straight way to sort condationaly in MongoDB, as per your example you can try aggregation query,
$facet to separate result for both types of documents
first, to get id: 3 documents
second, to get id is not 3 documents and sort by id in ascending order
$project and $concatArrays to concat both arrays in siquance
$unwind deconstruct all array
$replaceRoot to replace all object to root
db.collection.aggregate([
{
$facet: {
first: [
{ $match: { id: 3 } }
],
second: [
{ $match: { id: { $ne: 3 } } },
{ $sort: { id: 1 } }
]
}
},
{
$project: {
all: { $concatArrays: ["$first", "$second"] }
}
},
{ $unwind: "$all" },
{ $replaceRoot: { newRoot: "$all" } }
])
Playground

Mongoose sort by a field in the last element of a nested array

[{
_id: '1',
logs: [
{
createdAt: "2021-04-01",
},
{
createdAt: "2021-04-02",
},
],
},{
_id: '2',
logs: [
{
createdAt: "2021-04-03",
},
{
createdAt: "2021-04-04",
},
],
}]
I want to sort by the createdAt of the last element of logs in desc order. In this example item _id:2 should be the first and item _id:1 should be the second since 2021-04-02 is 'smaller' than 2021-04-04.
I tried 'logs[logs.length - 1].createdAt': -1 and 'logs.createdAt': -1 but they don't work.
To get sorted Object with respect to the last entry in each of the array,
you need to fetch the last date from the array which can be done using $last operator.
Next I am adding the field which has the entry of the last createdAt in a new field name - sortField.
I will sort it according to the sortField.
I will unset the sortField.
I know there would be better ways to do this, but it should do the job for the time being.
db.collection.aggregate([
{
"$addFields": {
"sortField": {
"$last": "$logs.createdAt"
}
}
},
{
$sort: {
"sortField": -1
}
},
{
$unset: "sortField"
}
])

find duplicates in array per document in mongodb

Let's say that I have some document with this structure:
_id: ObjectId('444455'),
name: 'test',
email: 'email,
points: {
spendable: 23,
history: [
{
comment: 'Points earned by transaction #1234',
points: 1
},
{
comment: 'Points earned by transaction #456',
points: 3
},
{
comment: 'Points earned by transaction #456',
points: 3
}
]
}
}
Now I have a problem that some documents contains duplicates objects in the points.history array.
Is there a way to easily find all those duplicates by a query?
I already tried this query: Find duplicate records in MongoDB
but that shows the total count of every duplicated line in all documents. I need a overview of the duplicates per document like this:
{
_id: ObjectId('444455') //_id of the document not of the array item itself
duplicates: [
{
comment: 'Points earned by transaction #456
}
]
}, {
_id: ObjectId('444456') //_id of the document not of the array item itself
duplicates: [
{
comment: 'Points earned by transaction #66234
},
{
comment: 'Points earned by transaction #7989
}
]
}
How can I achieve that?
Try below aggregate pipeline
collectionName.aggregate([
{
$unwind: "$points.history"
},
{
$group: {
_id: {
id: "$_id",
comment: "$points.history.comment",
points: "$points.history.points"
},
sum: {
$sum: 1
},
}
},
{
$match: {
sum: {
$gt: 1
}
}
},
{
$project: {
_id: "$_id._id",
duplicates: {
comment: "$_id.comment"
}
}
}
])

MongoDB sum with match

I have a collection with the following data structure:
{
_id: ObjectId,
text: 'This contains some text',
type: 'one',
category: {
name: 'Testing',
slug: 'test'
},
state: 'active'
}
What I'm ultimately trying to do is get a list of categories and counts. I'm using the following:
const query = [
{
$match: {
state: 'active'
}
},
{
$project: {
_id: 0,
categories: 1
}
},
{
$unwind: '$categories'
},
{
$group: {
_id: { category: '$categories.name', slug: '$categories.slug' },
count: { $sum: 1 }
}
}
]
This returns all categories (that are active) and the total counts for documents matching each category.
The problem is that I want to introduce two additional $match that should still return all the unique categories, but only affect the counts. For example, I'm trying to add a text search (which is indexed on the text field) and also a match for type.
I can't do this at the top of the pipeline because it would then only return categories that match, not only affect the $sum. So basically it would be like being able to add a $match within the $group only for the $sum. Haven't been able to find a solution for this and any help would be greatly appreciated. Thank you!
You can use $cond inside of your $group statement:
{
$group: {
_id: { category: '$categories.name', slug: '$categories.slug' },
count: { $sum: { $cond: [ { $eq: [ "$categories.type", "one" ] }, 1, 0 ] } }
}
}

MongoDB aggregation: $unwind after grouping by date

I have this model for purchases:
{
purchase_date: 2018-03-11 00:00:00.000,
total_cost: 400,
items: [
{
title: 'Pringles',
price: 200,
quantity: 2,
category: 'Snacks'
}
]
}
What I'm trying to do is to, first of all, to group the purchases by date, by doing so:
{$group: {
_id: {
date: $purchase_date,
items: '$items'
}
}}
However, now what I want to do is group the purchases of each day by items[].category and calculate how much was spent for each category in that day. I was able to do that with one day, but when I grouped each purchase by date I no longer able to $unwind the items.
I tried passing the path $items and it doesn't find it at all. If I try to use $_id.$items or _id.$items in both cases I get an error stating that it is not a valid path for $unwind.
You can use purchase_data and items.category as a grouping _id but you need to use $unwind on items before and then you can add another $group to get all groups per day
db.col.aggregate([
{ $unwind: "$items" },
{
$group: {
_id: {
purchase_date: "$purchase_date",
category: "$items.category",
},
total: { $sum: { $multiply: [ "$items.price", "$items.quantity" ] } }
}
},
{
$group: {
_id: "$_id.purchase_date",
categories: { $push: { name: "$_id.category", total: "$total" } }
}
}
])