MongoDB Mastery
MongoDB Mastery lessons / Module 6: Projection, Sort & Pagination

Projection, sorting and pagination

Level 1: Beginner20 min

Return only what you need, sort deterministically, and paginate without the skip trap.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • Use inclusion and exclusion projections
  • Project array slices and matching elements
  • Sort on multiple fields deterministically
  • Implement keyset (range-based) pagination

conceptThe concept

Projection is the second argument to find: { name: 1, email: 1 } (inclusion) or { items: 0 } (exclusion). You cannot mix them except for _id: 0. Array helpers: { items: { $slice: 2 } }, { items: { $elemMatch: { qty: { $gte: 3 } } } }.

Sort with .sort({ total: -1, _id: 1 }). Without a sort, order is not guaranteed. Include a unique tiebreaker (usually _id) so pages are stable.

Pagination: skip(n) still walks past n documents, so page 5,000 is slow. Keyset pagination remembers the last value seen and asks for the next range: find({ createdAt: { $lt: lastSeen } }).sort({ createdAt: -1 }).limit(20), with an index on the sort field.

whyWhy it matters

Projection reduces network traffic and memory, and enables covered queries. Unstable sorting causes duplicates or gaps between pages. Deep skip is one of the most common causes of slow APIs.

Use caseReal-world scenario

An admin order list shows 20 orders per page, newest first. Instead of ?page=812 with skip(16220), the API returns nextCursor = { createdAt, _id } of the last row and the next request filters past it.

Try itExamples

Run and edit these in the interactive lesson.

Projection
db.orders.findOne({}, { orderNo: 1, total: 1, "shipping.city": 1, _id: 0 })
db.orders.findOne({}, { items: 0, payment: 0 })
db.orders.findOne({ "items.2": { $exists: true } }, { orderNo: 1, items: { $slice: 1 } })
Deterministic sort
db.products.find({}, { name: 1, rating: 1 }).sort({ rating: -1, _id: 1 }).limit(5)
skip/limit vs keyset pagination
// page 3 with skip
db.orders.find({}, { orderNo: 1, createdAt: 1 }).sort({ createdAt: -1, _id: -1 }).skip(40).limit(3)

// keyset: remember the last row of the previous page
const page1 = db.orders.find({}, { orderNo: 1, createdAt: 1 }).sort({ createdAt: -1, _id: -1 }).limit(3).toArray()
const last = page1[page1.length - 1]
db.orders.find({ $or: [
  { createdAt: { $lt: last.createdAt } },
  { createdAt: last.createdAt, _id: { $lt: last._id } }
] }, { orderNo: 1, createdAt: 1 }).sort({ createdAt: -1, _id: -1 }).limit(3)
Counting
db.orders.estimatedDocumentCount()
db.orders.countDocuments({ status: "delivered" })

internalsUnder the hood

A sort that an index cannot provide becomes a blocking SORT stage limited to 100 MB of memory (it spills to disk if allowDiskUse is enabled, the default since 6.0 for find). With sort + limit, the server keeps only the top N documents in memory (a top-k sort), which is much cheaper.

Avoid / preferDo this, not that

Avoid
db.orders.find().sort({ createdAt: -1 }).skip(50000).limit(20)
Prefer
db.orders.find({ $or: [
  { createdAt: { $lt: last.createdAt } },
  { createdAt: last.createdAt, _id: { $lt: last._id } }
] }).sort({ createdAt: -1, _id: -1 }).limit(20)

skip still walks and discards 50,000 entries. Keyset pagination starts from the last row of the previous page, with _id as a tie-breaker so no row is skipped or repeated.

Avoid
db.orders.find().sort({ total: -1 })   // no index
Prefer
db.orders.createIndex({ total: -1 })
db.orders.find().sort({ total: -1 }).limit(10)

Without an index the server must read and sort everything in memory (and spill to disk past 100 MB). An index returns rows already in order, and limit stops early.

PitfallsCommon mistakes

  • Mixing inclusion and exclusion ({ name: 1, items: 0 }), which is an error.
  • Paginating with skip on large offsets.
  • Sorting on a non-unique field only, so ties come back in a different order on each page.
  • Using countDocuments() on every page view of a huge collection; consider an estimate or a cached count.

LimitsWhen not to use it

Offset pagination is fine for small, bounded lists such as an admin screen with a handful of pages. Keyset pagination cannot jump straight to page 37, so pick it for feeds and APIs, not for numbered page links.

SecuritySecurity notes

A projection is not access control: hiding a field in one API response does not stop another query from reading it. Restrict sensitive fields with views and roles. Do not let clients choose arbitrary sort or projection fields either; sorting by an unindexed field is an easy way to make the server work hard.

ChallengeTry it yourself

Return page 2 (rows 11 to 20) of delivered orders, highest total first, using keyset pagination: fetch page 1, then continue after its last row. Show only orderNo and total.

Show one solution
One solution
const page1 = db.orders.find({ status: "delivered" }, { orderNo: 1, total: 1 }).sort({ total: -1, _id: -1 }).limit(10).toArray()
const last = page1[page1.length - 1]
db.orders.find({
  status: "delivered",
  $or: [{ total: { $lt: last.total } }, { total: last.total, _id: { $lt: last._id } }]
}, { orderNo: 1, total: 1 }).sort({ total: -1, _id: -1 }).limit(10)

InterviewQuestions you should be able to answer

Why is skip-based pagination slow and what do you use instead?

skip(n) must examine and discard n index keys or documents before returning results, so latency grows linearly with the page number. Keyset pagination filters on the last seen sort key (plus _id as tiebreaker) and uses an index on that key, so every page costs the same.

countDocuments vs estimatedDocumentCount?

estimatedDocumentCount() reads collection metadata: instant, no filter, can be slightly off after an unclean shutdown or with orphans in sharded clusters. countDocuments(filter) runs an aggregation and is exact, but costs a scan of matching index keys or documents.

QuizCheck your understanding

4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.