I am new to NoSQL databases and MongoDb. I've got the following question:
I have a collection of such documents:
{
vals: [
{
value: 111,
timestamp: 1563454669669
},
{
value: 222,
timestamp: 1563454689665
},
{
value: 333,
timestamp: 1563454669658
}
.......
]
}
I would like to convert it into the following documents using aggregation pipeline:
{
vals: [
[
111,
1563454669669
],
[
222,
1563454689665
],
[
333,
1563454669658
]
.......
]
}
After years of work with relational databases, it's quite hard to understand..
You can use $map operator to transform one array into another array:
db.collection.aggregate([
{
$project: {
vals: {
$map: {
input: "$vals",
in: [ "$$this.value", "$$this.timestamp" ]
}
}
}
}
])
Mongo Playground
Related
If I have a document like this:
db.people.insertOne({name: "Annie", latestScore: 5});
Then based on this answer, I am able to move latestScore to an array field like this:
db.people.updateOne(
{name: 'Annie'},
[
{$set:
{scoreHistory:
{$concatArrays: [
{$ifNull: ["$scoreHistory", []]},
["$latestScore"]
]}
}
},
{ $unset: ["latestScore"] }
]
);
This is the resulting document:
{
_id: ObjectId("61d2737611e48e0d0c30b35b"),
name: 'Annie',
scoreHistory: [ 5 ]
}
Can we perform the same update to objects nested in an array? For example, if the document is like this:
db.people.insertOne({
name: 'Annie',
words: [
{word: "bunny", score: 5},
{word: "pink", score: 5}
]
});
How can we update it to this:
{
name: 'Annie',
words: [
{word: "bunny", scoreHistory: [5]},
{word: "pink", scoreHistory: [5]}
]
}
I know I can iterate and modify the array from the app and update the whole array at once, but I would like to do it using operators like in the first example above.
The website first displays words.word and words.score in rows, clicking on a row shows words.scoreHistory in a popup. I am expecting words array to be less than 500. Any advise on remodelling the schema to simplify the above operation is also welcome!
db.collection.update({
name: "Annie"
},
[
{
$set: {
words: {
$map: {
input: "$words",
as: "m",
in: {
word: "$$m.word",
scoreHistory: {
$concatArrays: [
{
$ifNull: [
"$$m.scoreHistory",
[]
]
},
[
"$$m.score"
]
]
}
}
}
}
}
}
])
mongoplayground
I have an array in a Mongo document that has a number of properties. I want to produce a document that has a single object that represents the array.
{
scores_array: [
{ name: "One", score: 40, key: "abc" },
{ name: "Two", score: 50, key: "def" },
{ name: "Three", score: 40, key: "abc" },
{ name: "Four", score: 40, key: "ghi" },
}
}
What I want is to be able to query for documents that have a particular value for two of the properties in the array, for example, { name: "One", score: 40 }.
I can use $in to find documents that have the matching key or value, and I could redact them to filter them out but I need to reference them again later in the same pipeline, so I'm trying to find a way to filter without removing anything from the documents.
My plan is to use $reduce to produce a new property that looks like this:
scores_summary: {
One: 40,
Two: 50,
Three: 40,
Four: 40,
}
I'm having trouble getting the $reduce syntax right. Here's what I've got:
db.test.aggregate([
{
$match: {
scores_array: {$exists: true}
}
},
{
$project: {
scores_summary: {
$reduce: {
input: "$scores_array",
initialValue: {},
"in": {
"$mergeObjects": [
"$$value",
{
"$$this.name":"$$this.score"
}
]
}
}
}
}
},
]);
This gives me "Unrecognized expression $$this.name". If I replace that with a constant, I get an output that looks right, except for that property.
Is there a reason I can't use $$this.name as the property name in the mergeObjects in a reduce? Hoping I'm just missing something.
(I'm using Mongo 3.4 currently which means I can't just fall back to $expr).
Thanks
You can use $map and $arrayToObject
db.collection.aggregate([
{
$match: {
scores_array: {
$exists: true
}
}
},
{
$project: {
scores_summary: {
"$arrayToObject": {
$map: {
input: "$scores_array",
"in": {
k: "$$this.name",
v: "$$this.score"
}
}
}
}
}
}
])
Working Mongo playground
this is my schema:
new Schema({
code: { type: String },
toy_array: [
{
date:{
type:Date(),
default: new Date()
}
toy:{ type:String }
]
}
this is my db:
{
"code": "Toystore A",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "buzz"
},
{
_id:"xxxxx", // automatic
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
_id:"xxxxx", // automatic
"toy": "jessie"
}
]
}
I am trying to update an object. In this case I want to update the document with code: 'ToystoreA' and add an array of subdocuments to the array named toy_array if the toys does not exists in the array.
for example if I try to do this:
db.mydb.findOneAndUpdate({
code: 'ToystoreA,
/*toy_array: {
$not: {
$elemMatch: {
toy: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},*/
},
{
$addToSet: {
toy_array: {
$each: [{"toy":'woddy'},{"toy":"buzz"}],
},
},
},
{
new: false,
}
})
they are added and is what I want to avoid.
how can I do it?
[
{
"code": "Toystore A",
"toy_array": [
{
"toy": "buzz"
},
{
"toy": "pope"
}
]
},
{
"code": "Toystore B",
"toy_array": [
{
"toy": "jessie"
}
]
}
]
In this example [{"toy":'woddy'},{"toy":"buzz"}] it should only be added 'woddy' because 'buzz' is already in the array.
Note:when I insert a new toy an insertion date is also inserted, in addition to an _id (it is normal for me).
As you're using $addToSet on an object it's failing for your use case for a reason :
Let's say if your document look like this :
{
_id: 123, // automatically generated
"toy": "buzz"
},
{
_id: 456, // automatically generated
"toy": "pope"
}
and input is :
[{_id: 789, "toy":'woddy'},{_id: 098, "toy":"buzz"}]
Here while comparing two objects {_id: 098, "toy":"buzz"} & {_id: 123, "toy":"buzz"} - $addToSet consider these are different and you can't use $addToSet on a field (toy) in an object. So try below query on MongoDB version >= 4.2.
Query :
db.collection.updateOne({"_id" : "Toystore A"},[{
$addFields: {
toy_array: {
$reduce: {
input: inputArrayOfObjects,
initialValue: "$toy_array", // taking existing `toy_array` as initial value
in: {
$cond: [
{ $in: [ "$$this.toy", "$toy_array.toy" ] }, // check if each new toy exists in existing arrays of toys
"$$value", // If yes, just return accumulator array
{ $concatArrays: [ [ "$$this" ], "$$value" ] } // If No, push new toy object into accumulator
]
}
}
}
}
}])
Test : aggregation pipeline test url : mongoplayground
Ref : $reduce
Note :
You don't need to mention { new: false } as .findOneAndUpdate() return old doc by default, if you need new one then you've to do { new: true }. Also if anyone can get rid of _id's from schema of array objects then you can just use $addToSet as OP was doing earlier (Assume if _id is only unique field), check this stop-mongoose-from-creating-id-property-for-sub-document-array-items.
In mongo I have a documents that follow the below pattern :
{
name: "test",
codes: [
[
{
code: "abc",
value: 123
},
{
code: "def",
value: 456
},
],
[
{
code: "ghi",
value: 789
},
{
code: "jkl",
value: 012
},
]
]
}
I'm using an aggregate query (because of joins) and in a $project block I need to return the "name" and the value of the object that has a code of "def" if it exists and an empty string if it doesn't.
I can't simply $unwind codes and $match because the "def" code is not guaranteed to be there.
$filter seems like the right approach as $elemMatch doesn't work, but its not obvious to me how to do this on nested array of arrays.
You can try below query, instead of unwinds & filter this can give you required result with less docs to operate on :
db.collection.aggregate([
/** merge all arrays inside codes array into code array */
{
$addFields: {
codes: {
$reduce: {
input: '$codes',
initialValue: [],
in: { $concatArrays: ["$$value", "$$this"] }
}
}
}
},
/** project only needed fields & value will be either def value or '',
* if 'def' exists in any doc then we're check index of it to get value of that particular object using arrayElemAt */
{
$project: {
_id:0, name: 1, value:
{
$cond: [{ $in: ["def", '$codes.code'] }, { $arrayElemAt: ['$codes.value', { $indexOfArray: ["$codes.code", 'def'] }] }, '']
}
}
}])
Test : MongoDB-Playground
Document Structure
{
_id: 5,
grades: [
{ grade_ : 80, mean: 75, std: 8 },
{ mean: 90, std: 5 },
{ mean: 85, std: 3 }
]
}
As per above document structure in mongodb i want rename key grade_ to grade
db.collection.update({"_id":5},{"$rename":{"grades.grade_":"grades.grade"}},{"upsert":false,"multi":true})
which gives below error
"writeError" : {
"code" : 28,
"errmsg" : "cannot use the part (grades of grades.grade_) to traverse the element ({grades: [ { grade_: 80.0, mean: 75.0, std: 8.0 }, { mean: 90.0, std: 5.0 }, { mean: 85.0, std: 3.0 } ]})"
}
I want to rename key grade_ to grade, expected output
{
_id: 5,
grades: [
{ grade : 80, mean: 75, std: 8 },
{ mean: 90, std: 5 },
{ mean: 85, std: 3 }
]
}
As per MongoDB documentation: ($rename does not work if these fields are in array elements.)
For fields in embedded documents, the $rename operator can rename these fields as well as move the fields in and out of embedded documents. $rename does not work if these fields are in array elements.
So, you need to write your custom logic to update.
db.collection.find({
"grades.grade_": { $exists : 1 }
}).forEach( function( doc ) {
for( i=0; i < doc.grades.length; i++ ) {
if(doc.grades[i].grade_ != undefined) {
doc.grades[i].grade = doc.grades[i].grade_;
delete doc.grades[i].grade_;
}
}
db.collection.update({ _id : doc._id }, doc);
});
$rename do not works in an array. So,you can use Aggregate framework's $addField to rename fields in an array.
db.collection.aggregate([
{
$addFields: {
grades: {
$map: {
input: "$grades",
as: "grade",
in: {
grade: "$$grade.grade_",
mean: "$$grade.mean",
std: "$$grade.std"
}
}
}
}
}
])
Output:
[
{
"_id": 5,
"grades": [
{"grade": 80,"mean": 75,"std": 8},
{"mean": 90,"std": 5},
{"mean": 85,"std": 3}
]
}
]