Projection, Sorting, Limiting and Pagination

Beginner
12 min

Projection, Sorting, Limiting and Pagination

find() answers "which documents?", but a real application also needs to control which fields come back, in what order, and how many at a time. MongoDB handles these with projection and the cursor methods sort(), limit() and skip(). After this lesson you will be able to return only the fields you need, sort reliably, page through results, and pick the pagination strategy that stays fast on large collections.

Projection: Choosing the Fields to Return

The second argument to find() is the projection. 1 includes a field, 0 excludes it; you cannot mix the two in one projection, except to switch off _id.

javascript
// Inclusion: only name and price (plus _id, which is included by default) db.products.find({}, { name: 1, price: 1 }) // Exclusion: everything except two large fields db.products.find({}, { description: 0, reviews: 0 }) // Nested fields use dot notation; _id: 0 may be combined with inclusion db.products.find({}, { name: 1, "specs.weight": 1, _id: 0 })

Arrays have dedicated projection operators: { reviews: { $slice: 3 } } returns the first three elements, { reviews: { $slice: [10, 5] } } skips ten and returns five, and { reviews: { $elemMatch: { rating: 5 } } } returns only the first element that matches.

Projection also cuts network traffic and enables covered queries when every projected field lives in an index.

Sorting

sort() takes a document whose keys are fields and whose values are 1 (ascending) or -1 (descending). Fields are applied in the order given.

javascript
db.products.find().sort({ price: -1 }) // most expensive first db.products.find().sort({ category: 1, price: -1 }) // by category, then by price

Rules worth remembering:

  • Documents missing the sort field sort as null, which comes before numbers and strings.
  • Without a supporting index MongoDB sorts in memory, with a 100 MB limit. Exceeding it raises Sort exceeded memory limit; .allowDiskUse() is a stop-gap, but the real fix is an index that matches the sort.
  • Equal keys have no guaranteed order. Add _id as a final tie-breaker so paging is stable.
  • Strings sort by UTF-8 byte order ("Zebra" before "apple"). For dictionary order pass a collation: .collation({ locale: "en", strength: 2 }).

limit() and skip()

javascript
db.products.find().sort({ price: 1 }).limit(5) // cheapest five db.products.find().sort({ price: 1 }).skip(5).limit(5) // the next five

Cursor methods are modifiers on a query that has not run yet, so their chaining order does not matter: the server always applies the filter, then the sort, then skip, then limit.

To count results use countDocuments(filter) rather than the deprecated count(). estimatedDocumentCount() returns the collection total from metadata almost instantly, which is ideal for dashboards, but it does not accept a filter.

Pagination Strategies

| Approach | Query | Strength | Weakness | |---|---|---|---| | Offset (skip + limit) | .skip((page - 1) * size).limit(size) | jump to any page number | the server walks every skipped document, so deep pages get slower and slower | | Keyset (range) | { _id: { $gt: lastId } } + .limit(size) | constant cost per page, stable under inserts | only next/previous navigation, needs a unique sortable key |

Keyset pagination works with any sort as long as a tie-breaker makes each position unique. For a list sorted by price descending:

javascript
// First page db.products.find().sort({ price: -1, _id: 1 }).limit(10) // Next page: everything strictly "after" the last document seen db.products.find({ $or: [ { price: { $lt: lastPrice } }, { price: lastPrice, _id: { $gt: lastId } } ] }).sort({ price: -1, _id: 1 }).limit(10)

Back this with a compound index on { price: -1, _id: 1 } and each page reads only the ten documents it returns.

Working with Cursors Programmatically

In scripts and drivers a cursor is consumed explicitly:

javascript
const cursor = db.products.find({ inStock: true }).batchSize(100); cursor.forEach(doc => print(doc.name)); // stream one document at a time const all = db.products.find().toArray(); // load everything into memory

Results arrive in batches and idle cursors are closed after 10 minutes. Prefer streaming with forEach for large result sets; toArray() on a million documents will exhaust client memory.

Common Mistakes

  • Mixing 1 and 0 in a projection. { name: 1, price: 0 } is rejected; only _id: 0 may be combined with inclusion.
  • Sorting without a tie-breaker. Two runs of the same page can return different rows.
Quick Quiz
Question 1 of 3

Which projection is invalid?

Key Takeaways

  • The projection argument selects fields: inclusion (1) or exclusion (0), never both, except for _id: 0.
  • sort() orders results and should be backed by an index once data grows; add _id as a tie-breaker.
  • skip() and limit() implement offset pagination; the server applies filter, sort, skip, then limit regardless of chaining order.
  • Keyset pagination ($gt the last key seen) is the scalable choice for large collections and infinite scroll.
  • Consume cursors with forEach, hasNext()/next() or toArray(), and count with countDocuments().

Next lesson: Query Operators: Comparison, Logical and Element — build precise filters with $gt, $in, $and, $or, $exists and $type.

Projection, Sorting, Limiting and Pagination - MongoDB | CodeYourCraft | CodeYourCraft