Query Documents
The find() method is MongoDB's primary query interface. It accepts an optional filter document that describes which documents to match and returns a cursor — a lazy iterator over the result set. You can chain cursor methods to sort, limit, skip, project, and transform results before fetching any data.
find() never loads all matching documents into memory at once; it fetches them in batches as you iterate. This makes it efficient for both tiny queries and large scans.
find() and findOne()
Use find() to retrieve multiple documents and findOne() to retrieve the first matching document (or null if nothing matches).
Basic find operations
// Find ALL documents in the collection (no filter = match everything)
db.users.find()
// Find all documents where role is "admin"
db.users.find({ role: "admin" })
// Find all active admins (implicit AND — both conditions must be true)
db.users.find({ role: "admin", active: true })
// Find the first matching document — returns a document or null
db.users.findOne({ email: "alice@example.com" })
// find() returns a cursor; call .toArray() to materialise results
const admins = db.users.find({ role: "admin" }).toArray()
// Limit the fields returned (projection: 1 = include, 0 = exclude)
db.users.find({ role: "admin" }, { name: 1, email: 1, _id: 0 })The Filter Document
The filter document passed to find() describes the shape of documents you want to match:
- An empty filter
{}matches every document in the collection. - A field: value pair performs an implicit
$eq(equality) check. - Multiple field: value pairs in the same object are combined with an implicit AND.
Filter examples
// Exact match — find users named "Alice"
db.users.find({ name: "Alice" })
// Implicit equality — same as { age: { $eq: 30 } }
db.users.find({ age: 30 })
// Multiple fields — implicit AND: name is "Alice" AND age is 30
db.users.find({ name: "Alice", age: 30 })
// Nested field query using dot notation
db.users.find({ "address.city": "Berlin" })
// Querying by a value in an array field
db.posts.find({ tags: "mongodb" }) // matches if "mongodb" appears anywhere in tags
// Comparison operators
db.products.find({ price: { $lt: 50 } }) // price < 50
db.products.find({ price: { $gte: 10, $lte: 100 } }) // 10 <= price <= 100
db.users.find({ age: { $ne: 18 } }) // age != 18
db.users.find({ role: { $in: ["admin", "editor"] } }) // role is admin or editorDot Notation for Nested Fields
To query fields inside an embedded document, use dot notation: "outerField.innerField". The quotes are required in JavaScript because the key contains a dot.
Querying nested documents
// Sample documents in the collection:
// { name: "Alice", address: { city: "Berlin", country: "DE", zip: "10115" } }
// { name: "Bob", address: { city: "Hamburg", country: "DE", zip: "20095" } }
// { name: "Carol", address: { city: "Vienna", country: "AT", zip: "1010" } }
// Find all users in Berlin
db.users.find({ "address.city": "Berlin" })
// → { name: "Alice", address: { city: "Berlin", ... } }
// Find all users in Germany (DE)
db.users.find({ "address.country": "DE" })
// → Alice, Bob
// Combine nested and top-level conditions
db.users.find({ "address.country": "DE", active: true })
// Deep nesting — works to any depth
db.orders.find({ "customer.address.city": "Berlin" })Querying Arrays
MongoDB's array query semantics are powerful: a field: value filter against an array field checks whether the array contains that value, not whether the array equals it.
Array queries
// Sample documents:
// { title: "Intro to MongoDB", tags: ["mongodb", "database", "nosql"] }
// { title: "Node.js Basics", tags: ["nodejs", "javascript"] }
// { title: "Full Stack Guide", tags: ["mongodb", "nodejs", "react"] }
// Match documents where tags contains "mongodb"
db.posts.find({ tags: "mongodb" })
// → "Intro to MongoDB", "Full Stack Guide"
// Match documents where tags contains BOTH "mongodb" AND "nodejs"
db.posts.find({ tags: { $all: ["mongodb", "nodejs"] } })
// → "Full Stack Guide"
// Match documents where tags contains AT LEAST ONE of the values
db.posts.find({ tags: { $in: ["mongodb", "react"] } })
// → All three documents
// Match documents where the tags array has exactly 2 elements
db.posts.find({ tags: { $size: 2 } })
// → "Node.js Basics"
// Querying array of embedded documents with $elemMatch
// (ALL conditions must match the SAME array element)
db.orders.find({
items: {
$elemMatch: { sku: "WIDGET-42", qty: { $gte: 5 } }
}
})The Cursor
find() returns a cursor — a pointer to the result set on the server. Documents are not fetched until you begin iterating. This lazy evaluation is efficient: if you only need the first 10 results, the server doesn't prepare the rest.
Working with cursors
// Materialise all results into a JavaScript array
const users = db.users.find({ active: true }).toArray()
// Iterate lazily with forEach (memory-efficient for large result sets)
db.users.find({ active: true }).forEach(user => {
console.log(user.name, user.email)
})
// Manual iteration with hasNext() / next()
const cursor = db.users.find({ role: "admin" })
while (cursor.hasNext()) {
const doc = cursor.next()
console.log(doc._id)
}
// Chain cursor modifiers before iterating
db.products
.find({ category: "widgets" })
.sort({ price: 1 }) // ascending by price
.skip(20) // skip first 20 (for pagination)
.limit(10) // return at most 10
.toArray()countDocuments() and estimatedDocumentCount()
Use countDocuments() to count documents matching a filter, or estimatedDocumentCount() for a fast approximate count of all documents in the collection (uses collection metadata, not a full scan).
// Count active users — performs a filtered scan
db.users.countDocuments({ active: true })
// Count all documents matching a complex filter
db.orders.countDocuments({
status: "shipped",
createdAt: { $gte: new Date("2024-01-01") }
})
// Fast approximate total — reads collection stats, not the full index
// Do not use with a filter — it ignores the filter argument
db.users.estimatedDocumentCount()Checking for Document Existence
A common pattern is to check whether a document exists before deciding what to do next. findOne() returns null when no document matches.
// Pattern: check existence, then branch
const existing = db.users.findOne({ email: "alice@example.com" })
if (existing) {
console.log("User already registered:", existing._id)
} else {
db.users.insertOne({ email: "alice@example.com", name: "Alice", createdAt: new Date() })
console.log("New user created")
}
// Lean existence check — only fetch the _id field (minimal data transfer)
const exists = db.users.findOne(
{ email: "alice@example.com" },
{ projection: { _id: 1 } } // fetch only _id
) !== nullThe explain() Method
explain() reveals how MongoDB executes a query — which indexes it used, how many documents it examined, and how long each stage took. It is the essential tool for diagnosing slow queries.
Analyzing query performance
// Get full execution statistics for a query
db.users.find({ email: "alice@example.com" }).explain("executionStats")
// Key fields to look for in the output:
// winningPlan.stage:
// "COLLSCAN" — full collection scan (no index used — slow on large collections)
// "IXSCAN" — index scan (fast, what you want)
// "FETCH" — fetch documents from disk using index results
//
// executionStats.totalDocsExamined — how many documents MongoDB read
// executionStats.totalKeysExamined — how many index keys were scanned
// executionStats.executionTimeMillis — total query time in milliseconds
//
// Ideal ratio: totalDocsExamined ≈ nReturned
// A COLLSCAN on a large collection with low nReturned → add an index
// Example: find users in Berlin — if no index on "address.city":
db.users.find({ "address.city": "Berlin" }).explain("executionStats")
// stage: "COLLSCAN", totalDocsExamined: 1_000_000, nReturned: 42
// After adding db.users.createIndex({ "address.city": 1 }):
// stage: "IXSCAN", totalDocsExamined: 42, nReturned: 42 ✓find() does not execute immediately — it returns a cursor. Documents are fetched from the server lazily as you iterate. Calling .toArray() or .forEach() triggers the actual query execution.findOne() when you expect exactly one result — it implicitly adds a limit: 1 to the query, allowing the server to stop scanning as soon as it finds the first match. It is slightly more efficient than find().limit(1).toArray()[0] and reads more clearly.db.collection.count() is deprecated as of MongoDB 4.0. Use countDocuments() for accurate filtered counts, or estimatedDocumentCount() for a fast approximate total count of all documents. The old count() can return inaccurate results on sharded clusters and after unclean shutdowns.