# Data Bits and Pieces

#### my $0.02 about data, technology, and other exciting randomness. ## Categorical Queries and Recency vs. Sorting in MongoDB 13 Jun 2013 ### Introduction This article describes a very specific use case of MongoDB, where we want to query for documents belonging to certain categories, and get the most recent n documents first. The two key conditions are therefore 1. Categories This means that your documents belong to one of a set of distinct values. Imagine news articles that can belong to a category like "Politics", "Technology", "Sports", etc. In this example we won't consider documents with multiple categories (tags), but thanks to MongoDB's multi-key indexes, the same principles can be extended to those cases as well. var result = db.documents .find({category: {"$in": ["Business", "Politics", "Sports"]}})

2. Recency

We are interested in "the most recent" n documents. This implies some sorting order, ensured by an index on a timestamp-like field (ts) and traditionally a sort and limit on the result set:

var n = 10000;
var result = db.documents

#### Index on {ts:-1, category:1}

The alternative is to use the reversed index, first on timestamp, descending, then on categories. The Index Tree Diagram would look something like the this:

Note that we use simple integers as timestamp value here, but this could be any sortable value, like dates or epoch numbers.

Now the documents are sorted globally, so any query can be returned in sorted order without in-memory sorts. But finding the matching categories requires to scan each of the index entries. Because the timestamp fields are all distinct from each other, each entry only links to a single document, with a single category entry. To retrieve the most recent n documents, MongoDB scans the index entries from left to right, filtering out any document that doesn't match the requested categories, until it has reached the limit. This scan is done in linear time, but how many documents need to be scanned? In the previous approach, we needed to look at C $\cdot$ L documents at most, where C is the number of categories, and L is the limit, but they needed to be sorted in memory. Here, it depends on the distribution of the data. In the worst case, there aren't enough matching documents, so every single document in the collection needs to be inspected. Not only take these collection scans a long time, they also mess up the working set in memory, in case you have more data than available RAM.

### Can we do better?

Remember that our initial requirement was less strict than what we attempted with the last two solutions. We didn't ask for sorted results, just for the most recent ones. One way to achieve this is by sorting and limiting. But there's another possibility, that avoids the expensive sort:

var total = db.documents.count();
var k = 10000;
var results = db.documents
.find({
category: {"$in": ["Business", "Politics", "Sports"]}, ts: {"$gt": total-k}})
.hint({category: 1, ts: -1})


Instead of sorting and limiting the results, we use a second query condition on the ts field. Here the timestamp has to be greater than k. Basically, we limit the number of documents before we match the categories, not after, as we did in the previous solutions. We also force the query to use the index on category first. How many documents will this query return? 10,000? Most likely not, unless the last 10,000 documents by chance all fall into the correct categories. That's unlikely, and for another find on different categories certainly not the case. Let's call the number of returned documents r, which is most likely not equal to n, the number of documents we wanted. How different r and n are depends on the distribution of documents over the categories, and on our choice of k, the document limit before we filter out the categories. The upside of this query is that it is very fast. Matching the categories is simply a matter of branching into each of the category btree children, and limiting the results means setting a lower bound on the range.

So while this query didn't really fit the brief of returning the most recent n matching documents, at least it ran very fast :-)

Let's recap: We have a fast way of checking the last k documents and filtering out the ones that match the categories. The query will return the most recent r matching documents, which may be different from the desired number of n documents returned:

• k number of documents to check for category match
• r number of matching documents returned after inspecting k documents: r ≤ k
• n number of desired documents matching

We'd like to change the query so that r is closer to, ideally identical to, n. Given a fixed distribution of data over the categories, there is only one variable that we can adjust, and that is k. Let's say we want to return the most recent n=5 documents, we queried with k=5 documents (the lower bound), and got r=3 documents back. That's not enough, so we repeat the query with a higher k=6. This time, we get 4 documents back, which is closer to n but doesn't quite reach it yet. A third query with k=7 does the trick, and we now have the most recent 5 documents, without relying on a .sort() in the query.

The graphic below shows this example for k=5, 6, 7. Each time we push the lower bound bracket (calculated as total-k) a little further to the right on each of the category branches, until the final results returns the desired number of documents.

With 3 fast queries (4 if we count the initial count), we have now found the most recent 5 documents that match the given categories without using a sort or having to scan through a lot of documents.

### Optimizing the Iterative Algorithm

There are a few more optimizations that we can make use of.

In above example, we linearly increased k from 5 to 6 to 7, but we don't have to search every single k. Instead, we can use a binary search on k until we get to the correct number of matching documents, which only requires log(n) steps in the worst case.

The second optimization is more of a suggestion to relax the conditions even a little further. Perhaps you don't really need exactly n most recent documents. Then you can make use of the fact, that with binary search, you can trade some precision for a bit more performance. If your use case allows a small error margin on n, you can iterate until the number of returned documents lies within the error bounds. This can have a dramatic effect on performance, as a lot of iteration steps would be used to get exactly to n matching documents. The algorithm below allows to specify a min and max value, and it will stop iterations when the number of matching documents lies within that range. Even an error margin of 5% can reduce the number of necessary iterations significantly. You can still specify a .limit(n) on the cursor, to get at most n documents. This allows for flexible use cases, for example:

Return exactly 100,000 documents, where these documents are at least in the most recent 105,000 documents (5% error margin).

For parallel queue processing, this may be an acceptable requirement, which can be answered much quicker than the sorted queries.

### Results

In this test I used 5 million documents separated into 100 categories, which isn't that much when you think about them as actors for a movie database, or product categories for an online shop for example.

I've ran both the sorted and the iterative version for different numbers of n: 100, 1000, 10000, 25000, 50000, 75000, 100000. The error margin for r was 10% (so anything between n and 1.1 $\cdot$ n was acceptable). Here are the results:

The x-axis is the number of most recent documents n to retrieve, the y-axis shows how long each query took in milliseconds. The numbers above each measurement point show how many documents (r) were actually retrieved. For 100,000 documents, the sorted query was roughly 8x slower than the iterative one. These results depend on your data distribution and the number of categories though. With less categories, the difference may not be as big. As always, test your queries in a staging/QA environment.

### Implementation

Here is a javascript function that finds the n most recent documents matching the categories given in query, where n will be between min and max. query needs to be of the form: {cat: {$in: [1, 55, 88]}} function findRecent(collection, query, min, max) { var total = db[collection].count(); var step, marker, cursor, c, last_marker, qc; step = min; marker = total; c = 0; while (c < min) { last_marker = marker; marker = marker - step; query['ts'] = {'$gt': marker};
cursor = db[collection].find(query).hint({cat:1, ts:-1});
c = cursor.count();
step = step*2;
}

if (c < max) {
return cursor;
}

step = (last_marker - marker) / 2;
marker = marker + step;

while (true) {
query['ts'] = {'\$gt': marker};
cursor = db[collection].find(query).hint({cat:1, ts:-1});
c = cursor.count();

step = step/2;

if (c > max) {
marker = marker + step;
} else if (c < min) {
marker = marker - step;
} else {
return cursor;
}
}
}


And if you want to reproduce these results, I used this little Python script to fill the database (using numbers 0–99 for categories):

from pymongo import MongoClient, ASCENDING, DESCENDING
from random import choice

categories = range(100)

mc = MongoClient("localhost:27017")
db = mc.test

db.docs.drop()

interval = 1000
docs = []
for i in xrange(5000000):
doc = {"cat": choice(categories), "ts": i}
docs.append(doc)

if i % interval == interval-1:
print i
db.docs.insert(docs, w=0)
docs = []