MongoDB Find and Queries

Read data from MongoDB with find and findOne, filter by exact values, combine conditions, query nested fields with dot notation, match values inside arrays, count matches, and understand cursors.

Lesson 3 of 43Beginner18 min read

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:

MongoDB Shell
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

MongoDB Shell
db.products.find()                              // every document
db.products.find({ category: 'kitchen' })       // documents matching the filter
db.products.findOne({ name: 'Lamp' })           // the first match, or null
  • find returns a cursor, which fetches results in batches. mongosh shows the first 20; type it for more. In code, use .toArray() or loop over it.
  • findOne returns a single document or null, which is handy when looking up by _id or another unique field.
  • Matching is exact and case-sensitive: { name: 'lamp' } doesn't find 'Lamp'. Types matter too: { price: '24' } doesn't match the number 24.

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 is null or 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 most n documents, 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:

MongoDB Shell
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 Ada

This 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.

Example 1

Exact matches and findOne

MongoDB Shell
db.products.find({ category: 'kitchen' })
db.products.findOne({ name: 'Lamp' }).price
db.products.findOne({ name: 'lamp' })
db.products.find({ price: '24' }).toArray().length
Output
[
  {
    _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
0
Line-by-line explanation
  1. Line 1The filter returns every document whose category is exactly kitchen.
  2. Line 2findOne returns a single document, so its fields can be read directly.
  3. Line 3Matching is case-sensitive, so 'lamp' finds nothing and findOne returns null.
  4. Line 4The string '24' doesn't match the number 24.
Example 2

Several conditions, nested fields and arrays

MongoDB Shell
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 })
Output
[ { name: 'Rug' } ]
[ { name: 'Mug' }, { name: 'Rug' } ]
[ { name: 'Lamp' } ]
[ { name: 'Pen' } ]
[ { name: 'Pen' } ]
Line-by-line explanation
  1. Line 1Two conditions must both be true; the second uses dot notation for a nested field.
  2. Line 1The second argument, a projection, returns only names to keep the output short.
  3. Line 2A single value matches any document whose tags array contains it.
  4. Line 3Dot notation works for any depth of nesting.
  5. Line 4null matches documents where the field is missing, like the Pen with no stock.
  6. Line 5An empty array matches documents whose array is exactly empty.
Example 3

Counting, distinct values and cursors

MongoDB Shell
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`))
Output
5
2
[ 'home', 'kitchen', 'office' ]
[ 'Leeds', 'York' ]
[ { name: 'Mug' }, { name: 'Lamp' } ]
Mug: 12 left
Kettle: 5 left
Rug: 2 left
Line-by-line explanation
  1. Lines 1 to 2countDocuments counts every document or only the matches.
  2. Line 3distinct lists each unique value once, sorted.
  3. Line 4distinct can be limited by a filter.
  4. Line 5limit caps how many documents the cursor returns.
  5. Line 6forEach runs a function for each document as the cursor fetches it.

Common MongoDB CRUD Mistakes

Searching with the wrong case

Don't
db.users.find({ email: 'Ada@Example.com' })     // stored as ada@example.com: no match
Do
db.users.find({ email: 'ada@example.com' })     // store emails lowercased and search lowercased

Nested field without quotes

Don't
db.products.find({ stock.warehouse: 'Leeds' })  // SyntaxError
Do
db.products.find({ 'stock.warehouse': 'Leeds' })

Matching a nested document exactly by accident

Don't
db.products.find({ stock: { warehouse: 'Leeds' } })   // needs the whole sub-document to be exactly this: no matches
Do
db.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.

Fill in the blankseasy

Complete the query to find the single user with id 7.

db.users.({ _id: 7 })
Multiple choiceeasy

What does find() return?

Predict the outputmedium

Using the products collection, how many documents does this match?

db.products.find({ category: 'kitchen', 'stock.warehouse': 'Leeds' })
Multiple choicemedium

What does { tags: 'sale' } match when tags is an array?

Multiple choicehard

What does { discount: null } match?

MongoDB CRUD Quiz

5 questions to check what you learned.

  1. How are several conditions in one filter combined?

    • OR
    • AND
    • XOR
    • Only the first is used
  2. 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
  3. What does findOne return when nothing matches?

    • null
    • An empty object
    • An error
    • An empty array
  4. Which method lists the unique values of a field?

    • unique()
    • group()
    • values()
    • distinct()
  5. Does { price: '10' } match a document where price is the number 10?

    • Yes
    • Only in mongosh
    • No, types must match
    • Only with an index