Storing data is only useful if you can get it back. In MongoDB, you read documents with find, which returns every document matching a filter, and findOne, which returns just the first match. The filter is itself a document describing what you're looking for: { category: 'kitchen' } means "documents whose category is kitchen".
Because documents can contain nested objects and arrays, MongoDB's queries can reach inside them. Dot notation such as 'address.city' matches a field inside an embedded document, and a filter on an array field matches documents where any element matches. Several conditions in one filter must all be true.
This lesson covers the everyday queries you'll write constantly: exact matches, several conditions, nested fields, arrays, missing fields, counting and listing distinct values. The next lesson adds operators such as greater than, in and or. All examples use this products collection:
db.products.insertMany([
{ _id: 1, name: 'Mug', category: 'kitchen', price: 8.5, tags: ['gift', 'ceramic'], stock: { qty: 12, warehouse: 'Leeds' } },
{ _id: 2, name: 'Lamp', category: 'home', price: 24, tags: ['lighting'], stock: { qty: 0, warehouse: 'York' } },
{ _id: 3, name: 'Kettle', category: 'kitchen', price: 29.99, tags: ['electric'], stock: { qty: 5, warehouse: 'Leeds' } },
{ _id: 4, name: 'Rug', category: 'home', price: 79, tags: ['gift'], stock: { qty: 2, warehouse: 'Leeds' } },
{ _id: 5, name: 'Pen', category: 'office', price: 1.2, tags: [] }
])find and findOne
db.products.find() // every document
db.products.find({ category: 'kitchen' }) // documents matching the filter
db.products.findOne({ name: 'Lamp' }) // the first match, or nullfindreturns a cursor, which fetches results in batches. mongosh shows the first 20; typeitfor more. In code, use.toArray()or loop over it.findOnereturns a single document ornull, which is handy when looking up by_idor another unique field.- Matching is exact and case-sensitive:
{ name: 'lamp' }doesn't find'Lamp'. Types matter too:{ price: '24' }doesn't match the number24.
Several conditions, nested fields and arrays
- Several fields in one filter are combined with AND:
{ category: 'home', price: 24 }. - Dot notation reaches into embedded documents:
{ 'stock.warehouse': 'Leeds' }. The field name must be in quotes. - Filtering an array field by one value matches documents where the array contains that value:
{ tags: 'gift' }. - Matching a whole array,
{ tags: ['gift', 'ceramic'] }, requires exactly those elements in that order. { field: null }matches documents where the field isnullor missing.
Counting and distinct values
countDocuments(filter)counts matching documents accurately.estimatedDocumentCount()returns a fast estimate of the whole collection's size, using metadata.distinct('field', filter)returns the unique values of a field, such as every category in use.find(filter).limit(n)returns at mostndocuments, which is useful for previews.
Looking up documents by ObjectId
A very common surprise is a lookup by _id that returns null even though the document is clearly there. An ObjectId is its own BSON type, not a string, so a filter containing the id's text doesn't match:
const { insertedId } = db.users.insertOne({ name: 'Ada' })
db.users.findOne({ _id: insertedId.toString() }) // null: a string isn't an ObjectId
db.users.findOne({ _id: new ObjectId(insertedId.toString()) }) // finds AdaThis happens most often in web apps, where the id arrives as text in a URL such as /users/65f1c2.... Convert it with new ObjectId(id) before querying. In Node.js, import ObjectId from the mongodb package and wrap the conversion in a try, because an invalid string throws an error rather than returning nothing.
The same rule about types applies to embedded documents. A filter such as { stock: { qty: 0, warehouse: 'York' } } compares the whole sub-document, including the order of its fields, so { warehouse: 'York', qty: 0 } wouldn't match the same document. Dot notation avoids both problems because it compares one field at a time.
MongoDB CRUD Syntax
Query basics
db.c.find()
db.c.find({ status: 'paid' })
db.c.find({ status: 'paid', country: 'UK' }) // AND
db.c.find({ 'address.city': 'Leeds' }) // nested field
db.c.find({ tags: 'sale' }) // array contains
db.c.findOne({ _id: 42 })
db.c.find({ deletedAt: null }) // null or missing
db.c.countDocuments({ status: 'paid' })
db.c.distinct('status')
db.c.find().limit(5).toArray()MongoDB CRUD Summary
- find(filter) returns a cursor of matching documents; findOne returns one document or null.
- Filters match exact values and are case- and type-sensitive.
- Several fields in a filter are combined with AND.
- Dot notation such as 'stock.warehouse' queries nested fields; array filters match any element.
- countDocuments counts matches; distinct lists unique values.
MongoDB CRUD Examples
Each example shows the code, the output it produces and what each part does.
Exact matches and findOne
db.products.find({ category: 'kitchen' })
db.products.findOne({ name: 'Lamp' }).price
db.products.findOne({ name: 'lamp' })
db.products.find({ price: '24' }).toArray().length[
{
_id: 1,
name: 'Mug',
category: 'kitchen',
price: 8.5,
tags: [ 'gift', 'ceramic' ],
stock: { qty: 12, warehouse: 'Leeds' }
},
{
_id: 3,
name: 'Kettle',
category: 'kitchen',
price: 29.99,
tags: [ 'electric' ],
stock: { qty: 5, warehouse: 'Leeds' }
}
]
24
null
0Line-by-line explanation
- Line 1The filter returns every document whose category is exactly kitchen.
- Line 2findOne returns a single document, so its fields can be read directly.
- Line 3Matching is case-sensitive, so 'lamp' finds nothing and findOne returns null.
- Line 4The string '24' doesn't match the number 24.
Several conditions, nested fields and arrays
db.products.find({ category: 'home', 'stock.warehouse': 'Leeds' }, { name: 1, _id: 0 })
db.products.find({ tags: 'gift' }, { name: 1, _id: 0 })
db.products.find({ 'stock.qty': 0 }, { name: 1, _id: 0 })
db.products.find({ stock: null }, { name: 1, _id: 0 })
db.products.find({ tags: [] }, { name: 1, _id: 0 })[ { name: 'Rug' } ]
[ { name: 'Mug' }, { name: 'Rug' } ]
[ { name: 'Lamp' } ]
[ { name: 'Pen' } ]
[ { name: 'Pen' } ]Line-by-line explanation
- Line 1Two conditions must both be true; the second uses dot notation for a nested field.
- Line 1The second argument, a projection, returns only names to keep the output short.
- Line 2A single value matches any document whose tags array contains it.
- Line 3Dot notation works for any depth of nesting.
- Line 4null matches documents where the field is missing, like the Pen with no stock.
- Line 5An empty array matches documents whose array is exactly empty.
Counting, distinct values and cursors
db.products.countDocuments()
db.products.countDocuments({ category: 'kitchen' })
db.products.distinct('category')
db.products.distinct('stock.warehouse', { category: 'home' })
db.products.find({}, { name: 1, _id: 0 }).limit(2)
db.products.find({ 'stock.warehouse': 'Leeds' }).forEach(p => print(`${p.name}: ${p.stock.qty} left`))5
2
[ 'home', 'kitchen', 'office' ]
[ 'Leeds', 'York' ]
[ { name: 'Mug' }, { name: 'Lamp' } ]
Mug: 12 left
Kettle: 5 left
Rug: 2 leftLine-by-line explanation
- Lines 1 to 2countDocuments counts every document or only the matches.
- Line 3distinct lists each unique value once, sorted.
- Line 4distinct can be limited by a filter.
- Line 5limit caps how many documents the cursor returns.
- Line 6forEach runs a function for each document as the cursor fetches it.
Common MongoDB CRUD Mistakes
Searching with the wrong case
db.users.find({ email: 'Ada@Example.com' }) // stored as ada@example.com: no matchdb.users.find({ email: 'ada@example.com' }) // store emails lowercased and search lowercasedNested field without quotes
db.products.find({ stock.warehouse: 'Leeds' }) // SyntaxErrordb.products.find({ 'stock.warehouse': 'Leeds' })Matching a nested document exactly by accident
db.products.find({ stock: { warehouse: 'Leeds' } }) // needs the whole sub-document to be exactly this: no matchesdb.products.find({ 'stock.warehouse': 'Leeds' })MongoDB CRUD in Real Projects
Queries in real applications
A product page fetches one document with findOne({ _id }). A category page uses find({ category }) with a limit and sort, and a search box adds more conditions. Admin dashboards show countDocuments for orders by status, and filter menus are filled with distinct. In Node.js the same filters are passed to the driver: await products.find({ tags: 'gift' }).toArray().
MongoDB CRUD FAQ
What is the difference between find and findOne in MongoDB?
find returns a cursor over every matching document, which you read with toArray(), a loop or forEach. findOne returns the first matching document itself, or null if nothing matches. Use findOne when you expect at most one result, such as a lookup by _id or a unique email.
How do I do a case-insensitive find in MongoDB?
For an occasional query, use a regular expression with the i flag: db.users.find({ name: /^ada$/i }). Anchoring it with ^ and $ stops it matching longer names. For queries you run often, store a lowercased copy of the value, or create an index with a case-insensitive collation and query with the same collation, so the index can be used.
How do I check whether a document exists without fetching it all?
Use db.users.countDocuments({ email: 'ada@example.com' }, { limit: 1 }), which returns 1 or 0 and stops at the first match. Alternatively, findOne with a projection such as { _id: 1 } returns a tiny document or null. Both are fast when the field is indexed.
Why does mongosh only show 20 documents?
mongosh prints the first batch of a cursor and then waits; type it to see the next 20. To see everything at once, call .toArray() on the cursor, or change the batch size with config.set('displayBatchSize', 50). Applications aren't affected, because drivers keep fetching batches as you iterate.
What is the difference between countDocuments and estimatedDocumentCount?
countDocuments(filter) actually runs a query and counts the matches, so it's exact and accepts a filter. estimatedDocumentCount() takes no filter and reads the collection's metadata, so it's almost instant even on huge collections, but it can be slightly off, for example after an unclean shutdown. Use the estimate for "about 2.3 million products" and the exact count when correctness matters.
MongoDB CRUD Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the query to find the single user with id 7.
db.users.({ _id: 7 })
What does find() return?
Using the products collection, how many documents does this match?
db.products.find({ category: 'kitchen', 'stock.warehouse': 'Leeds' })
What does { tags: 'sale' } match when tags is an array?
What does { discount: null } match?
MongoDB CRUD Quiz
5 questions to check what you learned.
How are several conditions in one filter combined?
- OR
- AND
- XOR
- Only the first is used
How do you query a field inside an embedded document?
- With dot notation in quotes, like 'address.city'
- With a JOIN
- With $nested
- It isn't possible
What does findOne return when nothing matches?
- null
- An empty object
- An error
- An empty array
Which method lists the unique values of a field?
- unique()
- group()
- values()
- distinct()
Does { price: '10' } match a document where price is the number 10?
- Yes
- Only in mongosh
- No, types must match
- Only with an index