MongoDB Projection, Sorting and Paging

Return only the fields you need with projections, order results with sort, page through them with limit and skip or range-based paging, and slice arrays in the results.

Lesson 5 of 43Beginner18 min read

A query decides which documents come back. Two more questions matter just as much in real applications: which fields come back, and in what order. A product list doesn't need every review and every image of every product; it needs names, prices and a thumbnail, sorted by price or popularity, twenty at a time.

MongoDB handles the first question with a projection, the second argument to find, which includes or excludes fields. Returning only what you need makes responses smaller and faster and stops sensitive fields, such as password hashes, from leaking out by accident.

The order and size of results are controlled with cursor methods: sort, limit and skip. Together they power sorted, paginated lists. In this lesson you'll shape results with projections, sort on one or several fields, page through results with skip and limit, and learn range-based paging, which stays fast however deep a user browses.

Projections

MongoDB Shell
db.products.find({}, { name: 1, price: 1 })          // include name and price (and _id)
db.products.find({}, { name: 1, price: 1, _id: 0 })  // ...without _id
db.products.find({}, { tags: 0, stock: 0 })          // everything except tags and stock
db.products.find({}, { 'stock.qty': 1, _id: 0 })     // a nested field
  • 1 includes a field and 0 excludes it. You can't mix including and excluding in one projection, except for _id, which is included unless you set _id: 0.
  • Dot notation projects fields inside embedded documents.
  • $slice returns part of an array: { reviews: { $slice: 3 } } gives the first three, -3 the last three.
  • Projections can also compute values using aggregation expressions, such as { name: 1, priceWithVat: { $multiply: ['$price', 1.2] } }.

Sorting

MongoDB Shell
db.products.find().sort({ price: 1 })             // ascending: cheapest first
db.products.find().sort({ price: -1 })            // descending
db.products.find().sort({ category: 1, price: -1 })   // by category, then price within each
  • 1 is ascending and -1 descending; the order of fields in the sort document matters.
  • Strings sort by character codes by default, so 'Zebra' comes before 'apple'. A collation such as .collation({ locale: 'en', strength: 2 }) sorts and compares case-insensitively.
  • Missing and null values sort before numbers and strings in ascending order.
  • Sorting large results without an index is slow and limited in memory, so add an index that matches common sorts.

Paging with limit and skip

MongoDB Shell
const page = 3, size = 20
db.products.find().sort({ price: 1, _id: 1 }).skip((page - 1) * size).limit(size)

skip still has to walk past all the skipped documents, so page 5,000 is much slower than page 1. Range-based paging (also called keyset or cursor paging) remembers the last value seen and asks for the next ones:

MongoDB Shell
db.products.find({ _id: { $gt: lastSeenId } }).sort({ _id: 1 }).limit(20)

It stays fast at any depth and doesn't skip or repeat items when new documents are added while someone is browsing.

Returning one element from an array

Projections can also trim arrays to the part you need. This matters for documents with long arrays, such as a post with hundreds of comments, where returning the whole array for every request wastes bandwidth. Using a posts document with a comments array:

MongoDB Shell
db.posts.findOne({}, { comments: { $slice: [1, 2] } })                    // skip 1, then return 2
db.posts.findOne({ 'comments.by': 'b' }, { 'comments.$': 1 })              // first element the filter matched
db.posts.findOne({}, { title: 1, comments: { $elemMatch: { by: 'b' } } })  // first element matching this condition
  • $slice: [skip, n] works like skip and limit for one array; $slice: n alone returns the first n, and a negative number counts from the end.
  • The positional $ returns the first array element that matched the query filter, so the filter must include a condition on that array.
  • $elemMatch in a projection has its own condition, independent of the filter, and also returns only the first matching element.

$slice keeps the other fields of the document unless you exclude them, while comments.$ and $elemMatch follow the normal include rules, so add any other fields you want, such as title: 1. If you need every matching element rather than the first, use the $filter expression in an aggregation pipeline instead.

MongoDB CRUD Syntax

Shaping results

db.c.find(filter, { a: 1, b: 1, _id: 0 })
db.c.find(filter, { secret: 0 })
db.c.find({}, { comments: { $slice: -5 } })
db.c.find().sort({ createdAt: -1 })
db.c.find().sort({ category: 1, price: -1, _id: 1 })
db.c.find().skip(40).limit(20)
db.c.find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(20)
db.c.find().collation({ locale: 'en', strength: 2 }).sort({ name: 1 })

MongoDB CRUD Summary

  • The projection (second argument of find) includes fields with 1 or excludes them with 0.
  • _id is returned unless you exclude it with _id: 0; don't otherwise mix 1s and 0s.
  • sort({ field: 1 }) is ascending, -1 descending; several fields sort in order.
  • limit and skip page through results; skip slows down for deep pages.
  • Range-based paging with a unique sort key is fast at any depth.

MongoDB CRUD Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Projections

MongoDB Shell
db.products.findOne({ _id: 1 }, { name: 1, price: 1 })
db.products.findOne({ _id: 1 }, { name: 1, price: 1, _id: 0 })
db.products.findOne({ _id: 1 }, { tags: 0, stock: 0, category: 0 })
db.products.find({ stock: { $exists: true } }, { name: 1, 'stock.qty': 1, _id: 0 })
db.products.findOne({ _id: 1 }, { name: 1, firstTag: { $first: '$tags' }, _id: 0 })
Output
{ _id: 1, name: 'Mug', price: 8.5 }
{ name: 'Mug', price: 8.5 }
{ _id: 1, name: 'Mug', price: 8.5 }
[
  { name: 'Mug', stock: { qty: 12 } },
  { name: 'Lamp', stock: { qty: 0 } },
  { name: 'Kettle', stock: { qty: 5 } },
  { name: 'Rug', stock: { qty: 2 } }
]
{ name: 'Mug', firstTag: 'gift' }
Line-by-line explanation
  1. Line 1Including fields returns only those, plus _id.
  2. Line 2_id: 0 is the one exclusion allowed alongside inclusions.
  3. Line 3Excluding fields returns everything else.
  4. Line 4Dot notation projects a single field from inside a nested document.
  5. Line 5Projections can compute new fields with aggregation expressions such as $first.
Example 2

Sorting on one and several fields

MongoDB Shell
db.products.find({}, { name: 1, price: 1, _id: 0 }).sort({ price: -1 }).limit(3)
db.products.find({}, { category: 1, name: 1, _id: 0 }).sort({ category: 1, name: 1 })
db.products.find({}, { name: 1, 'stock.qty': 1, _id: 0 }).sort({ 'stock.qty': 1 }).limit(2)
Output
[
  { name: 'Rug', price: 79 },
  { name: 'Kettle', price: 29.99 },
  { name: 'Lamp', price: 24 }
]
[
  { name: 'Lamp', category: 'home' },
  { name: 'Rug', category: 'home' },
  { name: 'Kettle', category: 'kitchen' },
  { name: 'Mug', category: 'kitchen' },
  { name: 'Pen', category: 'office' }
]
[ { name: 'Pen' }, { name: 'Lamp', stock: { qty: 0 } } ]
Line-by-line explanation
  1. Line 1-1 sorts from the highest price, and limit keeps the top three.
  2. Line 2Documents are sorted by category first, then by name within each category.
  3. Line 3The Pen has no stock field, and missing values sort before numbers.
Example 3

Page by page

MongoDB Shell
const size = 2
for (let page = 1; page <= 3; page++) {
  const names = db.products.find({}, { name: 1 }).sort({ _id: 1 })
    .skip((page - 1) * size).limit(size).toArray().map(p => p.name)
  print(`Page ${page}: ${names.join(', ')}`)
}

let lastId = 0
let batch
do {
  batch = db.products.find({ _id: { $gt: lastId } }).sort({ _id: 1 }).limit(2).toArray()
  if (batch.length) {
    print(`After id ${lastId}: ${batch.map(p => p.name).join(', ')}`)
    lastId = batch[batch.length - 1]._id
  }
} while (batch.length)
Output
Page 1: Mug, Lamp
Page 2: Kettle, Rug
Page 3: Pen
After id 0: Mug, Lamp
After id 2: Kettle, Rug
After id 4: Pen
Line-by-line explanation
  1. Lines 3 to 4skip passes over the earlier pages and limit takes the next two.
  2. Lines 8 to 15Range-based paging asks for documents after the last id seen.
  3. Line 11The filter on _id uses an index, so it stays fast however far you go.
  4. Line 13The last id of each batch becomes the starting point for the next.

Common MongoDB CRUD Mistakes

Mixing inclusion and exclusion

Don't
db.users.find({}, { name: 1, password: 0 })   // error: cannot do exclusion in inclusion projection
Do
db.users.find({}, { name: 1, email: 1 })        // list what you want

Paging without a stable sort

Don't
db.products.find().sort({ price: 1 }).skip(20).limit(20)        // equal prices can reorder between pages
Do
db.products.find().sort({ price: 1, _id: 1 }).skip(20).limit(20)

Returning sensitive fields

Don't
db.users.findOne({ _id: id })                    // includes passwordHash and resetToken
Do
db.users.findOne({ _id: id }, { passwordHash: 0, resetToken: 0 })

MongoDB CRUD in Real Projects

Shaping results in applications

API endpoints project only the fields the client needs, which keeps responses small on mobile networks and avoids exposing internal data. Infinite scrolling feeds, such as social timelines and order histories, use range-based paging on a date and _id, while admin tables with page numbers use skip and limit on smaller collections. Indexes are designed to match the most common sort orders.

MongoDB CRUD FAQ

How do I exclude the _id field from MongoDB results?

Add _id: 0 to the projection: db.products.find({}, { name: 1, price: 1, _id: 0 }). _id is the only field you can exclude inside an inclusion projection. Without _id: 0, it's always returned, even when you only list other fields.

How do I sort MongoDB results by date, newest first?

Sort on the date field in descending order: db.orders.find().sort({ createdAt: -1 }). This only works correctly when dates are stored as real Date values, not strings. Add _id: -1 as a second key if several documents can share the same timestamp, and create an index on { createdAt: -1 } for large collections.

Does the order of sort, skip and limit matter in a MongoDB query?

Not for find. The server always applies sort first, then skip, then limit, whatever order you chain the cursor methods in. It does matter in an aggregation pipeline, where stages run in exactly the order you write them, so a $limit before $sort sorts only the first few documents.

How do I get the document with the highest value in a field?

Sort descending and take one: db.products.find().sort({ price: -1 }).limit(1). With an index on price this reads just one index entry, however big the collection is. To get the lowest value, sort ascending instead.

How do I do a case-insensitive sort in MongoDB?

Add a collation with strength 2: db.products.find().sort({ name: 1 }).collation({ locale: 'en', strength: 2 }). Without it, uppercase letters sort before all lowercase ones. For large collections, create an index with the same collation so the sort can use it.

MongoDB CRUD Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the sort so the newest orders come first.

db.orders.find().sort({ createdAt:  })
Multiple choiceeasy

What does the projection { name: 1, _id: 0 } return?

Predict the outputmedium

With 45 matching documents and 20 per page, how many documents does page 3 return?

db.c.find().sort({ _id: 1 }).skip(40).limit(20)
Multiple choicemedium

Why add _id to a sort on price?

Multiple choicehard

Why does range-based paging stay fast when skip doesn't?

MongoDB CRUD Quiz

5 questions to check what you learned.

  1. Which value sorts in ascending order?

    • 1
    • -1
    • 0
    • 'asc'
  2. Where does the projection go in find?

    • First argument
    • In sort
    • Second argument
    • In limit
  3. What does { comments: { $slice: -3 } } return in a projection?

    • The first three comments
    • The last three comments
    • Comments except three
    • An error
  4. Where do documents missing the sort field appear in an ascending sort?

    • At the end
    • They're excluded
    • In random places
    • At the start
  5. Can you exclude _id while including other fields?

    • No
    • Yes, _id is the only field you can exclude in an inclusion projection
    • Only with aggregation
    • Only if _id is an ObjectId