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.
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.
// 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.
sort() takes a document whose keys are fields and whose values are 1 (ascending) or -1 (descending). Fields are applied in the order given.
db.products.find().sort({ price: -1 }) // most expensive first
db.products.find().sort({ category: 1, price: -1 }) // by category, then by priceRules worth remembering:
null, which comes before numbers and strings.Sort exceeded memory limit; .allowDiskUse() is a stop-gap, but the real fix is an index that matches the sort._id as a final tie-breaker so paging is stable."Zebra" before "apple"). For dictionary order pass a collation: .collation({ locale: "en", strength: 2 }).db.products.find().sort({ price: 1 }).limit(5) // cheapest five
db.products.find().sort({ price: 1 }).skip(5).limit(5) // the next fiveCursor 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.
| 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:
// 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.
In scripts and drivers a cursor is consumed explicitly:
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 memoryResults 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.
1 and 0 in a projection. { name: 1, price: 0 } is rejected; only _id: 0 may be combined with inclusion.Which projection is invalid?
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.$gt the last key seen) is the scalable choice for large collections and infinite scroll.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.