🔍 Querying Documents in MongoDB

Querying is the process of retrieving specific documents from a MongoDB collection using a query filter — a JSON-like object that describes the conditions a document must satisfy. MongoDB's query language is expressive and supports everything from simple equality checks to complex logical, array, and regular-expression matching. This tutorial covers the full range of query capabilities, from fundamentals to performance optimization.

Information

Examples use the official MongoDB Node.js Driver and assume a collection named products.

📖 1. Introduction

Every read operation in MongoDB revolves around a query document — an object whose keys represent fields to match and whose values describe the matching criteria. Queries can range from a simple exact match to deeply nested combinations of operators.

A basic query

// Simple equality query
const results = await collection.find({ category: "electronics" }).toArray();

🧱 2. Query Fundamentals

A query filter is passed as the first argument to find() or findOne(). An empty filter {} matches every document in the collection.

Query Anatomy
Field
Operator (optional)
Value

Implicit vs. explicit equality

// Implicit equality
{ status: "active" }

// Explicit equality using $eq
{ status: { $eq: "active" } }

Note

When no operator is specified, MongoDB assumes an equality match for that field.

âš–ī¸ 3. Comparison Operators

Comparison operators let you match values that are greater than, less than, or otherwise numerically or lexically related to a given value.

OperatorMeaning
$eqEqual to
$neNot equal to
$gtGreater than
$gteGreater than or equal to
$ltLess than
$lteLess than or equal to
$inMatches any value in an array
$ninMatches none of the values in an array

Using comparison operators

// Products priced between 20 and 100
const results = await collection.find({
  price: { $gte: 20, $lte: 100 }
}).toArray();

// Products in a specific set of categories
const inCategories = await collection.find({
  category: { $in: ["electronics", "accessories"] }
}).toArray();

🔗 4. Logical Operators

Logical operators combine multiple query conditions using AND, OR, NOT, and NOR semantics.

OperatorDescription
$andAll conditions must be true
$orAt least one condition must be true
$notNegates a condition
$norNone of the conditions may be true

Combining conditions with logical operators

// Implicit AND (multiple fields)
{ inStock: true, price: { $lt: 50 } }

// Explicit $or
{
  $or: [
    { inStock: true },
    { price: { $lt: 10 } }
  ]
}

// $and with repeated field conditions
{
  $and: [
    { price: { $gt: 10 } },
    { price: { $lt: 100 } }
  ]
}

Tip

MongoDB implicitly ANDs top-level fields, so you only need $and when combining multiple conditions on the same field or nesting complex logic.

đŸ§Ŧ 5. Element Operators

Element operators check for the existence or type of a field, rather than its value.

Using $exists and $type

// Documents where "discount" field exists
{ discount: { $exists: true } }

// Documents where "price" is a number
{ price: { $type: "number" } }

🧠 6. Evaluation Operators

Evaluation operators perform higher-level logic such as regular expression matching, modulo arithmetic, or evaluating a $expr aggregation expression within a query.

Evaluation operators in action

// Regex match (case-insensitive)
{ name: { $regex: "^mouse", $options: "i" } }

// Modulo: even stock counts
{ stockCount: { $mod: [2, 0] } }

// Compare two fields using $expr
{ $expr: { $gt: ["$sold", "$stockCount"] } }

Important

$expr allows you to use aggregation expressions inside a query, enabling comparisons between fields of the same document.

📚 7. Array Operators

Array operators query fields containing arrays, matching based on element presence, size, or structure.

OperatorDescription
$allArray contains all specified values
$elemMatchAt least one element matches all conditions
$sizeArray has an exact length

Querying array fields

// Array contains both "sale" and "featured"
{ tags: { $all: ["sale", "featured"] } }

// At least one review has rating >= 4 and verified: true
{
  reviews: {
    $elemMatch: { rating: { $gte: 4 }, verified: true }
  }
}

// Array with exactly 3 elements
{ tags: { $size: 3 } }

🧮 8. Bitwise Operators

Bitwise operators test integer field values at the bit level, useful for flags packed into a single numeric field.

Bitwise operators

// Bits at positions [0, 2] are set to 1
{ permissions: { $bitsAllSet: [0, 2] } }

// All specified bits are clear (0)
{ permissions: { $bitsAllClear: [1, 3] } }

Caution

Bitwise operators are a niche feature — most applications rarely need them outside of flag-based permission systems.

🔤 9. Regular Expressions

MongoDB supports PCRE-compatible regular expressions for pattern matching on string fields, either via $regex or native regex literals in the driver.

Native regex literal

// Names starting with "wireless" (case-insensitive)
const results = await collection.find({
  name: /^wireless/i
}).toArray();

Warning

Regex queries that don't anchor to the start of the string (^) cannot use an index efficiently and may scan the entire collection.

🆔 10. Query by _id

Every document has a unique _id field, typically an ObjectId. Querying by _id is the fastest possible lookup since it's always indexed.

Finding a document by its ObjectId

const { ObjectId } = require("mongodb");

const product = await collection.findOne({
  _id: new ObjectId("64f1a2b3c4d5e6f7890a1b2c")
});

đŸ—ƒī¸ 11. Nested Document Queries

When a field contains an embedded document, you can query it either as a whole object or by targeting a specific nested field.

Querying embedded documents

// Exact match on the entire embedded document
{ address: { city: "Chennai", zip: "600001" } }

// Match a single nested field (order-independent, preferred)
{ "address.city": "Chennai" }

Note

Matching the entire embedded document requires an exact match, including field order. Dot notation is almost always the safer choice.

đŸ“Ļ 12. Array Queries

Querying array fields can target the whole array, a single element, or elements at a specific index.

Array element matching

// Matches if the array contains "sale" anywhere
{ tags: "sale" }

// Matches if the element at index 0 is "sale"
{ "tags.0": "sale" }

đŸŽ¯ 13. Dot Notation

Dot notation is the standard way to reach into nested documents and arrays using a string path like "field.subfield".

Using dot notation for deep paths

// Nested field inside an array of embedded documents
{ "reviews.author": "Alice" }

// Deeply nested path
{ "shipping.address.country": "India" }

Hint

Dot notation paths must be quoted as strings since JavaScript object keys cannot contain literal dots.

đŸ—‚ī¸ 14. Projection

Projection controls which fields are returned, reducing payload size and improving performance.

Applying a projection

// Include only name and price
const results = await collection
  .find({ inStock: true }, { projection: { name: 1, price: 1, _id: 0 } })
  .toArray();

â†•ī¸ 15. Sorting

Use sort() to order query results. 1 sorts ascending, -1 sorts descending.

Multi-field sorting

const results = await collection
  .find({ category: "electronics" })
  .sort({ price: -1, name: 1 })
  .toArray();

đŸ”ĸ 16. Limiting Results

limit() caps the number of documents returned by a query.

Limiting query results

const topFive = await collection
  .find({})
  .sort({ price: -1 })
  .limit(5)
  .toArray();

â­ī¸ 17. Skipping Results

skip() bypasses a number of documents, often combined with limit() for pagination.

Paginating results

const page3 = await collection
  .find({})
  .sort({ _id: 1 })
  .skip(20)
  .limit(10)
  .toArray();

Caution

skip() degrades in performance on large offsets. For deep pagination, prefer range-based queries using the last seen _id.

đŸ–ąī¸ 18. Cursor Methods

find() returns a cursor — a pointer to the result set that can be iterated lazily instead of loading everything into memory at once.

Loading all results into memory

const docs = await collection.find({}).toArray();

Iterating with forEach()

await collection.find({}).forEach(doc => {
  console.log(doc.name);
});

Streaming with async iteration

const cursor = collection.find({});
for await (const doc of cursor) {
  console.log(doc.name);
}

Tip

Prefer forEach() or async iteration over toArray() when working with large result sets to avoid loading everything into memory at once.

🧾 19. Distinct Values

The distinct() method returns all unique values for a given field across matching documents.

Finding distinct field values

const categories = await collection.distinct("category");
console.log(categories);
// [ "electronics", "accessories", "furniture" ]

⚡ 20. Query Optimization

Efficient queries rely on indexes. Without an index, MongoDB must perform a collection scan, examining every document to find matches.

Creating indexes to support queries

// Create an index on the "price" field
await collection.createIndex({ price: 1 });

// Compound index for common query patterns
await collection.createIndex({ category: 1, price: -1 });

Reference

Design indexes based on your actual query patterns — see the MongoDB indexing guide for strategy details.

💡 21. Query Hints

hint() forces MongoDB to use a specific index, overriding the query planner's default choice — useful when you know a better index exists.

Forcing a specific index

const results = await collection
  .find({ category: "electronics", price: { $gt: 50 } })
  .hint({ category: 1, price: -1 })
  .toArray();

🧭 22. Explain Plans

The explain() method reveals how MongoDB executes a query — including whether it used an index (IXSCAN) or scanned the whole collection (COLLSCAN).

Analyzing query execution

const plan = await collection
  .find({ category: "electronics" })
  .explain("executionStats");

console.log(plan.executionStats.executionTimeMillis);
console.log(plan.queryPlanner.winningPlan.stage);

Example

A winningPlan.stage of "COLLSCAN" signals a missing index for that query pattern.

🚀 23. Performance Considerations

  • Index fields used frequently in filters, sorts, and joins ($lookup).
  • Use covered queries — where projection matches an index exactly — to avoid reading full documents.
  • Avoid leading wildcard regexes; they cannot use an index efficiently.
  • Keep compound indexes ordered by equality, then sort, then range fields (ESR rule).
  • Use explain() regularly during development to catch slow queries early.

🌟 24. Best Practices

  • Always project only the fields you need instead of returning full documents.
  • Prefer $in over multiple $or clauses on the same field for readability and performance.
  • Validate and sanitize any user-supplied values before placing them into a filter.
  • Use dot notation rather than exact embedded-document matches for flexibility.
  • Monitor slow queries using MongoDB's profiler or Atlas Performance Advisor.

âš ī¸ 25. Common Mistakes

  1. Using unanchored regex patterns on large collections, causing full scans.
  2. Forgetting quotes around dot-notation paths, resulting in invalid JavaScript.
  3. Assuming { tags: ["sale"] } matches an array containing only "sale", when it actually requires an exact array match.
  4. Using deep skip() values for pagination on large collections, hurting performance.
  5. Not checking explain() output before deploying a new query pattern to production.

Best Practice

When in doubt about how a nested condition will match, test it against a small sample using findOne() before running it at scale.

❓ 26. Frequently Asked Questions

Yes. Passing {} as the filter to find() returns every document in the collection.

Yes, using the $size operator, e.g. { tags: { $size: 3 } } matches arrays with exactly three elements.

By default, yes. Pass the i flag (e.g. { $options: "i" } or /pattern/i) for case-insensitive matching.

📌 27. Summary

>>"The best index is the one that matches how you actually query your data — not the one you think you might need someday."

Summary

Mastering MongoDB's query language unlocks precise, efficient data retrieval. Next, explore the aggregation framework and indexing strategies to build queries that scale.