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
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 field1includes a field and0excludes 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.
$slicereturns part of an array:{ reviews: { $slice: 3 } }gives the first three,-3the last three.- Projections can also compute values using aggregation expressions, such as
{ name: 1, priceWithVat: { $multiply: ['$price', 1.2] } }.
Sorting
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 each1is ascending and-1descending; 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
nullvalues 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
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:
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:
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 likeskipandlimitfor one array;$slice: nalone returns the firstn, 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. $elemMatchin 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.
Projections
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 }){ _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
- Line 1Including fields returns only those, plus _id.
- Line 2_id: 0 is the one exclusion allowed alongside inclusions.
- Line 3Excluding fields returns everything else.
- Line 4Dot notation projects a single field from inside a nested document.
- Line 5Projections can compute new fields with aggregation expressions such as $first.
Sorting on one and several fields
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)[
{ 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
- Line 1-1 sorts from the highest price, and limit keeps the top three.
- Line 2Documents are sorted by category first, then by name within each category.
- Line 3The Pen has no stock field, and missing values sort before numbers.
Page by page
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)Page 1: Mug, Lamp
Page 2: Kettle, Rug
Page 3: Pen
After id 0: Mug, Lamp
After id 2: Kettle, Rug
After id 4: PenLine-by-line explanation
- Lines 3 to 4skip passes over the earlier pages and limit takes the next two.
- Lines 8 to 15Range-based paging asks for documents after the last id seen.
- Line 11The filter on _id uses an index, so it stays fast however far you go.
- Line 13The last id of each batch becomes the starting point for the next.
Common MongoDB CRUD Mistakes
Mixing inclusion and exclusion
db.users.find({}, { name: 1, password: 0 }) // error: cannot do exclusion in inclusion projectiondb.users.find({}, { name: 1, email: 1 }) // list what you wantPaging without a stable sort
db.products.find().sort({ price: 1 }).skip(20).limit(20) // equal prices can reorder between pagesdb.products.find().sort({ price: 1, _id: 1 }).skip(20).limit(20)Returning sensitive fields
db.users.findOne({ _id: id }) // includes passwordHash and resetTokendb.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.
Complete the sort so the newest orders come first.
db.orders.find().sort({ createdAt: })
What does the projection { name: 1, _id: 0 } return?
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)
Why add _id to a sort on price?
Why does range-based paging stay fast when skip doesn't?
MongoDB CRUD Quiz
5 questions to check what you learned.
Which value sorts in ascending order?
- 1
- -1
- 0
- 'asc'
Where does the projection go in find?
- First argument
- In sort
- Second argument
- In limit
What does { comments: { $slice: -3 } } return in a projection?
- The first three comments
- The last three comments
- Comments except three
- An error
Where do documents missing the sort field appear in an ascending sort?
- At the end
- They're excluded
- In random places
- At the start
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