Slow pagination over tons of records in mongodb

Mongodb

Mongodb Problem Overview


I have over 300k records in one collection in Mongo.

When I run this very simple query:

db.myCollection.find().limit(5);

It takes only few miliseconds.

But when I use skip in the query:

db.myCollection.find().skip(200000).limit(5)

It won't return anything... it runs for minutes and returns nothing.

How to make it better?

Mongodb Solutions


Solution 1 - Mongodb

One approach to this problem, if you have large quantities of documents and you are displaying them in sorted order (I'm not sure how useful skip is if you're not) would be to use the key you're sorting on to select the next page of results.

So if you start with

db.myCollection.find().limit(100).sort({created_date:true});

and then extract the created date of the last document returned by the cursor into a variable max_created_date_from_last_result, you can get the next page with the far more efficient (presuming you have an index on created_date) query

db.myCollection.find({created_date : { $gt : max_created_date_from_last_result } }).limit(100).sort({created_date:true}); 

Solution 2 - Mongodb

From MongoDB documentation:

> Paging Costs > > Unfortunately skip can be (very) costly and requires the server to walk from the beginning of the collection, or index, to get to the offset/skip position before it can start returning the page of data (limit). As the page number increases skip will become slower and more cpu intensive, and possibly IO bound, with larger collections. > > Range based paging provides better use of indexes but does not allow you to easily jump to a specific page.

You have to ask yourself a question: how often do you need 40000th page? Also see this article;

Solution 3 - Mongodb

I found it performant to combine the two concepts together (both a skip+limit and a find+limit). The problem with skip+limit is poor performance when you have a lot of docs (especially larger docs). The problem with find+limit is you can't jump to an arbitrary page. I want to be able to paginate without doing it sequentially.

The steps I take are:

  1. Create an index based on how you want to sort your docs, or just use the default _id index (which is what I used)
  2. Know the starting value, page size and the page you want to jump to
  3. Project + skip + limit the value you should start from
  4. Find + limit the page's results

It looks roughly like this if I want to get page 5432 of 16 records (in javascript):

let page = 5432;
let page_size = 16;
let skip_size = page * page_size;

let retval = await db.collection(...).find().sort({ "_id": 1 }).project({ "_id": 1 }).skip(skip_size).limit(1).toArray();
let start_id = retval[0].id;

retval = await db.collection(...).find({ "_id": { "$gte": new mongo.ObjectID(start_id) } }).sort({ "_id": 1 }).project(...).limit(page_size).toArray();

This works because a skip on a projected index is very fast even if you are skipping millions of records (which is what I'm doing). if you run explain("executionStats"), it still has a large number for totalDocsExamined but because of the projection on an index, it's extremely fast (essentially, the data blobs are never examined). Then with the value for the start of the page in hand, you can fetch the next page very quickly.

Solution 4 - Mongodb

i connected two answer.

the problem is when you using skip and limit, without sort, it just pagination by order of table in the same sequence as you write data to table so engine needs make first temporary index. is better using ready _id index :) You need use sort by _id. Than is very quickly with large tables like.

db.myCollection.find().skip(4000000).limit(1).sort({ "_id": 1 });

In PHP it will be

$manager = new \MongoDB\Driver\Manager("mongodb://localhost:27017", []);
$options = [
			'sort' => array('_id' => 1),
		 	'limit' => $limit, 
		  	'skip' => $skip,
		  	
		];
$where = [];
$query = new \MongoDB\Driver\Query($where, $options );
$get = $manager->executeQuery("namedb.namecollection", $query);

Solution 5 - Mongodb

My collection has around 1.3M documents (not that big), properly indexed, but still takes a big performance hit by the issue.

After reading other answers, the solution forward is clear; the paginated collection must be sorted by a counting integer similar to the auto-incremental value of SQL instead of the time-based value.

The problem is with skip; there is no other way around it; if you use skip, you are bound to hit with the issue when your collection grows.
Using a counting integer with an index allows you to jump using the index instead of skip. This won't work with time-based value because you can't calculate where to jump based on time, so skipping is the only option in the latter case.

On the other hand,
by assigning a counting number for each document, the write performance would take a hit; because all documents must be inserted sequentially. This is fine with my use case, but I know the solution is not for everyone.
The most upvoted answer doesn't seem applicable to my situation, but this one does. (I need to be able to seek forward by arbitrary page number, not just one at a time.)

Plus, it is also hard if you are dealing with delete, but still possible because MongoDB support $inc with a minus value for batch updating. Luckily I don't have to deal with the deletion in the app I am maintaining.

Just write this down as a note to my future self. It is probably too much hassle to fix this issue with the current application I am dealing with, but next time, I'll build a better one if I were to encounter a similar situation.

Solution 6 - Mongodb

I'm going to suggest a more radical approach. Combine skip/limit (as an edge case really) with sort range based buckets and base the pages not on a fixed number of documents, but a range of time (or whatever your sort is). So you have top-level pages that are each range of time and you have sub-pages within that range of time if you need to skip/limit, but I suspect the buckets can be made small enough to not need skip/limit at all. By using the sort index this avoids the cursor traversing the entire inventory to reach the final page.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRadek SimkoView Question on Stackoverflow
Solution 1 - MongodbRussellView Answer on Stackoverflow
Solution 2 - MongodbTomasz NurkiewiczView Answer on Stackoverflow
Solution 3 - MongodbMr. TView Answer on Stackoverflow
Solution 4 - MongodbKamil DÄ…browskiView Answer on Stackoverflow
Solution 5 - MongodbCurious SamView Answer on Stackoverflow
Solution 6 - MongodbNovaterataView Answer on Stackoverflow