MongoDB: How to speed up my data reorganisation query/operation? - mongodb

I'm trying to analyse some data and I thought my queries would be faster ultimately by storing a relationship between my collections instead. So I wrote something to do the data normalisation, which is as follows:
var count = 0;
db.Interest.find({'PersonID':{$exists: false}, 'Data.DateOfBirth': {$ne: null}})
.toArray()
.forEach(function (x) {
if (null != x.Data.DateOfBirth) {
var peep = { 'Name': x.Data.Name, 'BirthMonth' :x.Data.DateOfBirth.Month, 'BirthYear' :x.Data.DateOfBirth.Year};
var person = db.People.findOne(peep);
if (null == person) {
peep._id = db.People.insertOne(peep).insertedId;
//print(peep._id);
}
db.Interest.updateOne({ '_id': x._id }, {$set: { 'PersonID':peep._id }})
++count;
if ((count % 1000) == 0) {
print(count + ' updated');
}
}
})
This script is just passed to mongo.exe.
Basically, I attempt to find an existing person, if they don't exist create them. In either case, link the originating record with the individual person.
However this is very slow! There's about 10 million documents and at the current rate it will take about 5 days to complete.
Can I speed this up simply? I know I can multithread it to cut it down, but have I missed something?

In order to insert new persons into People collection, use this one:
db.Interest.aggregate([
{
$project: {
Name: "$Data.Name",
BirthMonth: "$Data.DateOfBirth.Month",
BirthYear: "$Data.DateOfBirth.Year",
_id: 0
}
},
{
$merge: {
into: "People",
// requires an unique index on {Name: 1, BirthMonth: 1, BirthYear: 1}
on: ["Name", "BirthMonth", "BirthYear"]
}
}
])
For updating PersonID in Interest collection use this pipeline:
db.Interest.aggregate([
{
$lookup: {
from: "People",
let: {
name: "$Data.Name",
month: "$Data.DateOfBirth.Month",
year: "$Data.DateOfBirth.Year"
},
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ["$Name", "$$name"] },
{ $eq: ["$BirthMonth", "$$month"] },
{ $eq: ["$BirthYear", "$$year"] }
]
}
}
},
{ $project: { _id: 1 } }
],
as: "interests"
}
},
{
$set: {
PersonID: { $first: "$interests._id" },
interests: "$$REMOVE"
}
},
{ $merge: { into: "Interest" } }
])
Mongo Playground

Related

MongoDB sorting does not work with inner array

I'm trying to query specific fields in my document and sort them by one of the fields, however, the engine seems to completely ignore the sort.
I use the query:
db.symbols.find({_id:'AAPL'}, {'income_statement.annual.totalRevenue':1,'income_statement.annual.fiscalDateEnding':1}).sort({'income_statement.annual.totalRevenue': 1})
This is the output:
[
{
_id: 'AAPL',
income_statement: {
annual: [
{
fiscalDateEnding: '2021-09-30',
totalRevenue: '363172000000'
},
{
fiscalDateEnding: '2020-09-30',
totalRevenue: '271642000000'
},
{
fiscalDateEnding: '2019-09-30',
totalRevenue: '256598000000'
},
{
fiscalDateEnding: '2018-09-30',
totalRevenue: '265595000000'
},
{
fiscalDateEnding: '2017-09-30',
totalRevenue: '229234000000'
}
]
}
}
]
I would expect to have the entries sorted by fiscalDateEnding, starting with 2017-09-30 ascending.
However, the order is fixed, even if I use -1 for sorting.
Any ideas?
The sort you are using is for the ordering of documents in the result set. This is different from the ordering of array elements inside the document.
For your case, if you are using a newer version of MongoDB (5.2+), you can use the $sortArray.
db.symbols.aggregate([
{
$project: {
_id: 1,
annual: {
$sortArray: {
input: "$income_statement.annual",
sortBy: {
fiscalDateEnding: 1
}
}
}
}
}
])
If you are using older version of MongoDB, you can do the followings to perform the sorting.
db.collection.aggregate([
{
"$unwind": "$income_statement.annual"
},
{
$sort: {
"income_statement.annual.fiscalDateEnding": 1
}
},
{
$group: {
_id: "$_id",
annual: {
$push: "$income_statement.annual"
}
}
},
{
"$project": {
_id: 1,
income_statement: {
annual: "$annual"
}
}
}
])
Here is the Mongo Playground for your reference.

translate sql query into mongodb query

I'm trying to grasp the mongodb concepts by translating some of our sql queries into mongo aggregation framework.
I have an sql code:
select dbo.VisitNo(u.id) as visitNo , o.id, o.PatientId, u.VisitDate
from dbo.Observation o
join sbo.ProspectiveFollowUp u on u.rootid = o.Id
order by o.PatientId
The dbo.VisitNo is implemented as:
CREATE FUNCTION dbo.VisitNo(#Id int)
RETURNS INT
AS
BEGIN
DECLARE #VisitDate date, #RootId int
SELECT #VisitDate=VisitDate, #RootId=RootId FROM dbo.ProspectiveFollowUp WHERE Id=#Id
RETURN (SELECT COUNT(1) FROM dbo.ProspectiveFollowUp WHERE RootId = #RootId AND VisitDate <= #VisitDate)
END
result:
My document in Mongo has following structure:
{
"_id",
"values":[
{
"Id",
"PatientId",
"ProspectiveFollowUp":[
"Id",
"RootId",
"VisitDate"
]
}
]
}
The values array has always one element, but that's how the data was imported. ProspectiveFollowUp has at least one record.
Creating query for retrieving the data was rather easy:
db.dbo_ObservationJSON.aggregate([
{ $unwind: '$values' },
{
$project: {
_id: 0,
Id: '$values.Id',
PatientId: '$values.PatientId',
VisitDate: '$values.ProspectiveFollowUp.VisitDate'
}
},
{ $unwind: '$VisitDate' },
{ $sort: { PatientId: 1 } }
])
The harder part is the custom function itself. I can't think outside od tsql world yet, so I have hard time getting this to work. I have translated the function into mongo the following way:
var id = 4
var result = db.dbo.ObservationJSON.aggregate([
{ $unwind: '$values' },
{ $unwind: '$values.ProspectiveFollowUp' },
{ $project: { Id: '$values.ProspectiveFollowUp.Id', RootId: '$values.ProspectiveFollowUp.RootId', VisitDate: '$values.ProspectiveFollowUp.VisitDate', _id:0 }},
{ $match: { Id: id }}
]).toArray()[0]
var totalResult = db.dbo_ObservationJSON.aggregate([{
$unwind: {
path: '$values'
}
}, {
$unwind: {
path: '$values.ProspectiveFollowUp'
}
}, {
$project: {
Id: '$values.ProspectiveFollowUp.Id',
RootId: '$values.ProspectiveFollowUp.RootId',
VisitDate: '$values.ProspectiveFollowUp.VisitDate'
}
}, {
$match: {
RootId: result.RootId,
VisitDate: {
$lte: result.VisitDate
}
}
},{$count: 'total'}]).toArray()[0]
But don't know how to integrate it into the aggregation function above.
Can I write the entire sql query equivalent into one mongo aggregate expression?
I finally got it to work.
db.dbo_ObservationJSON.aggregate([
{ $unwind: '$values' },
{ $unwind: { path: '$values.ProspectiveFollowUp', "includeArrayIndex": "index" } },
{
$project: {
_id: 0,
VisitNo: { $add: ['$index', 1] },
RootId: '$values.ProspectiveFollowUp.RootId',
PatientId: '$values.PatientId',
VisitDate: '$values.ProspectiveFollowUp.VisitDate'
}
},
{
$sort: {
PatientId: 1
}
}
]);

Delete document that has size greater than a specific value

I have a collection which contains a multiple documents whose size has increased from 16MBs or is about to reach 16MBs.
I want query that finds documents which have size greater than 10MBs and delete all of them.
I am using following to find the size of document.
Object.bsonsize(db.test.findOne({type:"auto"}))
Is there a way to embed this query inside db.test.deleteMany() query?
This following query deletes the documents with size greater than the specified size (the size is specified in bytes). This query is valid with MongoDB v4.4 or higher.
db.collection.deleteMany( {
$expr: { $gt: [ { $bsonSize: "$$ROOT" }, SIZE_LIMIT ] },
type: "auto"
} )
The following script runs for MongoDB v4.2 or earlier:
const SIZE_LIMIT = 75 // substitute your value here in bytes
let idsToDelete = [ ]
let crsr = db.collection.find()
while(crsr.hasNext()) {
let doc= crsr.next()
if (Object.bsonsize(doc) > SIZE_LIMIT) {
idsToDelete.push(doc._id)
}
}
db.collection.deleteMany( { _id: { $in: idsToDelete } } )
I think you have to do it like this:
db.test.aggregate([
{ $match: { type: "auto" } },
{ $project: { bsonSize: { $bsonSize: "$$ROOT" } } },
{ $match: { bsonSize: { $gt: 16e6 } } },
]).forEach(function (doc) {
db.test.deleteOne({ _id: doc._id });
})
Or if you prefer deleteMany:
var ids = db.test.aggregate([
{ $match: { type: "auto" } },
{ $project: { bsonSize: { $bsonSize: "$$ROOT" } } },
{ $match: { bsonSize: { $lt: 16e6 } } }
]).toArray().map(x => x._id);
db.test.deleteMany({ _id: { $in: ids } });

Mongodb aggregation pass argument to element size of $sample

Hello every body here any one can help me with query below
I want to get quiz list with random amount
the amount of rendom will
base on each lesson
The problem is
mongodb not allow to pass argument to element size of $sample
Any one can give me the solution
lessonModel.aggregate([
{ $match : {'quiz.status':1 } },
{
$lookup : {
from : 'quiz',
let : { 'lesson_id' : '$_id','limit' : '$quiz.amount' },
pipeline : [
{
$match: {
$expr: {
$eq: [ "$lesson_id", "$$lesson_id" ]
}
}
},
{
$project: {
title:1,
check_list:1,
duration:1
}
},
{ $sample: { size: '$$limit' } }
],
as: 'quiz'
}
},
{$unwind: '$quiz'},
{ $replaceRoot: { newRoot: "$quiz" } }
]).exec();
The error said size argument to $sample must be a number
Here is my sample data
UPDATE
I think your main problem is to randomly pick amount number of quizs under each lesson. Since $sample is not helpful use $function (New in version MongoDB 4.4).
Solution
Inside $function operator write some logic to
Shuffle the questions (You can change it to your requirement).
Slice it to return the number(amount) of questions required.
db.lessons.aggregate([
{ $match: { "quiz.status": 1 } },
{
$lookup: {
from: "quiz",
let: { "lesson_id": "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$lesson_id", "$$lesson_id"] }
}
},
{
$project: {
title: 1,
check_list: 1,
duration: 1
}
}
],
as: "questions"
}
},
{
$project: {
quiz: {
$function: {
body: function(questions, amount) {
if (amount == 0) return [];
for (let i = questions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[questions[i], questions[j]] = [questions[j], questions[i]];
}
return questions.slice(0, amount);
},
args: ["$questions", { $ifNull: ["$quiz.amount", 0] }],
lang: "js"
}
}
}
},
{ $unwind: "$quiz" },
{ $replaceRoot: { newRoot: "$quiz" } }
]);
$sample does not support variables. A number must be specified explicitly like:
{
$sample: { size: 1 }
}
Also replace your let as shown below because last lesson document has no amount filed in the quiz object
let: {
'lesson_id': '$_id',
'limit': { $ifNull: ['$quiz.amount', 0] } // or any other number.
},
Wrong:
{
$sample: { size: "$$limit" } // Wont work!
}

mongoose updateOne function: don't update if $pull didn't work

I'm using updateOne method like this:
Photo.updateOne(
{
"_id": photoId
},
{
"$pull": {
comments: {
_id: ObjectID(commentId),
"user.id": user.id
}
},
"$inc": { "commentCount": -1 },
},
)
Photo model which contains comments as a array and commentCount as a number. When I run the code it's working but if the photo doesn't have the comment (which I'm trying to pull) it's still incrementing commentCount by -1. What I want is, if the code does not pull any comment in photo comments, don't update the commentCount too. How can I add this rule to my code?
Thanks for help.
You can also add both fields comments._id and comments.use.id conditions in query part, if comment is not available then it will skip update and pull part.
Photo.updateOne(
{
_id: photoId,
comments: {
$elemMatch: {
_id: ObjectID(commentId),
"user.id": user.id
}
}
},
{
"$pull": {
comments: {
_id: ObjectID(commentId),
"user.id": user.id
}
},
"$inc": { "commentCount": -1 }
}
)
There is no such feature existing in Mongo, What you can do if you're using Mongo v4.2+ is use pipelined update, as the name suggests this gives you the power to use a pipeline within an update, hence allowing us to have conditions based on previous results.
Photo.updateOne(
{ "_id": photoId },
[
{
$set: {
comments: {
$filter: {
input: "$comments",
as: "comment",
cond: {
$and: [
{$ne: ["$$comment._id", ObjectID(commentId)]},
{$ne: ["$$comment.user.id", user.id]} //really necessary?
]
}
}
}
}
},
{
$set: {
commentCount: {$size: "$comments"}
}
}
]
)
For lesser versions you'll have to split it into 2 calls. no way around it.
-------------- EDIT ---------------
You can update the query to find the document using $elemMatch, if it's not found then it means the comment belonged to someone else and you can throw an error in that case.
Photo.updateOne(
{
_id: photoId,
comments: {
$elemMatch: {
_id: objectID(commentId),
"user.id": user.id
}
}
},
{
"$pull": {
comments: {
_id: ObjectID(commentId),
"user.id": user.id
}
},
"$inc": { "commentCount": -1 }
}
)