← Back to Index

Query
Operators

FILE 08_query_operators
TOPIC $eq · $ne · $gt · $gte · $lt · $lte · $in · $nin · $regex · $mod
LEVEL Foundation
01
Comparison Overview
All comparison operators and where they are used
Reference

Comparison operators let you filter documents by evaluating field values against a specified value. They are the backbone of every non-trivial query in MongoDB.

General Syntax

// Standard form — field compared against operator value
db.collection.find({ field: { $operator: value } })

// Shorthand equality — no operator needed
db.collection.find({ field: value })

All Comparison Operators

Operator Meaning Example
$eq Equal to { age: { $eq: 30 } }
$ne Not equal to (includes missing field) { status: { $ne: "inactive" } }
$gt Greater than { price: { $gt: 100 } }
$gte Greater than or equal to { score: { $gte: 90 } }
$lt Less than { qty: { $lt: 10 } }
$lte Less than or equal to { rating: { $lte: 3 } }
$in Matches any value in array { color: { $in: ["red","blue"] } }
$nin Matches none of the values in array (includes missing field) { role: { $nin: ["admin","root"] } }
$cmp Aggregation only — returns -1, 0, or 1 { $cmp: ["$price", 100] }

Where Comparison Operators Are Used

Comparison operators work identically across all contexts that accept a query filter:

  • find() and findOne() — primary read filter
  • updateOne() / updateMany() — the first argument (filter)
  • deleteOne() / deleteMany() — the filter argument
  • Aggregation pipeline $match stage — same syntax as find()
NOTE
$cmp is exclusively an aggregation expression operator and cannot be used in find() filters. Use it inside $project, $addFields, or $match with $expr.
02
Equality: $eq / $ne
Exact match and its inverse — including the missing-field trap
Equality

$eq — Explicit Equality

$eq matches documents where the field value is exactly equal to the specified value. It behaves identically to the shorthand { field: value } syntax but has specific use cases where the explicit form is preferred.

// Shorthand — most common form
db.products.find({ category: "electronics" })

// Explicit $eq — functionally identical output
db.products.find({ category: { $eq: "electronics" } })

// Prefer explicit $eq when building queries programmatically
const filter = {}
filter[userProvidedField] = { $eq: userProvidedValue }
// Safer than: filter[userProvidedField] = userProvidedValue
// Explicit form prevents accidental operator injection
db.products.find(filter)

$eq vs Shorthand Comparison

ScenarioShorthand$eqResult
Value is a plain scalar { age: 30 } { age: { $eq: 30 } } Identical behavior
Field contains an array { tags: "mongodb" } { tags: { $eq: "mongodb" } } Both match if any array element equals value
Programmatic key injection Unsafe if value is an object Safe — value is treated as a scalar target Use $eq for dynamic query building

$ne — Not Equal

$ne matches documents where the field's value is not equal to the specified value. It also matches documents where the field does not exist at all — this is a common source of subtle bugs.

// Find all products NOT in category "electronics"
db.products.find({ category: { $ne: "electronics" } })
// WARNING: also returns docs where 'category' field is absent

// Tightened query — field must exist AND not equal value
db.products.find({
  category: { $exists: true, $ne: "electronics" }
})
TIP
When you truly mean "the field exists and its value is not X", always pair $ne with $exists: true. Using $ne alone will silently include documents that are missing the field entirely, which is rarely the intended behavior.
03
Range: $gt $gte $lt $lte
Numeric, date, string, and ObjectId range queries
Range

Basic Range Queries

// Greater than
db.products.find({ price: { $gt: 100 } })

// Greater than or equal to
db.products.find({ score: { $gte: 90 } })

// Less than
db.products.find({ qty: { $lt: 10 } })

// Less than or equal to
db.products.find({ rating: { $lte: 3 } })

Combining to Create a Bounded Range

Multiple comparison operators on the same field are ANDed together automatically. This is the standard way to express a range like "between 100 and 500 inclusive".

// Price between 100 (exclusive) and 500 (inclusive)
db.products.find({ price: { $gt: 100, $lte: 500 } })

// Equivalent using $and (more verbose, same result)
db.products.find({
  $and: [
    { price: { $gt: 100 } },
    { price: { $lte: 500 } }
  ]
})

Works With Multiple Types

TypeComparison BasisExample
Numbers Numeric magnitude { age: { $gte: 18 } }
Dates Chronological order (milliseconds since epoch) { createdAt: { $gte: new Date("2024-01-01") } }
Strings Lexicographic (dictionary) order per Unicode code points { name: { $gt: "M" } }
ObjectIds Chronological (timestamp embedded in first 4 bytes) { _id: { $gt: ObjectId("...") } }

Date Range Query

// Find orders placed in January 2024
db.orders.find({
  createdAt: {
    $gte: new Date("2024-01-01T00:00:00Z"),
    $lt:  new Date("2024-02-01T00:00:00Z")
  }
})

String Comparison Gotcha

String comparisons in MongoDB use Unicode code point order (lexicographic), not numeric order. This means "200" is considered less than "90" because the character "2" has a lower code point than "9".

// Gotcha: "200" < "90" lexicographically
// "2" (U+0032) sorts before "9" (U+0039)
db.col.find({ code: { $gt: "100" } })
// "200", "300" match but so does "9", "99"
// because "9" > "1" lexicographically

// Solution: store numeric-intent values as numbers, not strings
db.col.find({ code: { $gt: 100 } })  // numeric — correct ordering
WARN
If you need to range-query on values that represent numbers (order IDs, version numbers, etc.), store them as BSON numeric types, not strings. Lexicographic order will produce incorrect results on string-encoded numbers.
04
Set: $in / $nin
Match against a set of values — with regex and ObjectId support
Set

$in — Match Any Value in Array

$in matches documents where the field value equals any one of the values in the provided array. It is the most efficient way to express "field equals A or B or C" on a single field.

// Match any of three status values
db.orders.find({ status: { $in: ["pending", "processing", "shipped"] } })

// $in vs $or for same-field multi-value — $in is preferred
// These are functionally equivalent but $in is cleaner and faster:
db.orders.find({ $or: [
  { status: "pending" },
  { status: "processing" },
  { status: "shipped" }
]})
// Use $or for DIFFERENT fields; use $in for SAME field, multiple values

$in With ObjectIds

// Must wrap each value in ObjectId() — strings won't match
db.users.find({
  _id: {
    $in: [
      ObjectId("507f191e810c19729de860ea"),
      ObjectId("507f1f77bcf86cd799439011")
    ]
  }
})

$in With Regex Values

// $in array can contain regex literals
db.products.find({
  name: { $in: [/^mongo/i, /^redis/i] }
})
// Matches documents where name starts with "mongo" or "redis" (case-insensitive)

$nin — Match None of the Values

$nin matches documents where the field value is not present in the provided array. Like $ne, it also matches documents where the field is absent entirely.

// Exclude specific roles
db.users.find({ role: { $nin: ["admin", "superuser", "root"] } })
// WARNING: also returns docs where 'role' field does not exist

// Tightened: field must exist AND not be in the set
db.users.find({
  role: { $exists: true, $nin: ["admin", "superuser", "root"] }
})
WARN
$nin includes documents with missing fields — the same gotcha as $ne. Pair it with $exists: true when you only want to filter documents that actually have the field set.
05
Pattern: $regex / $mod
Pattern matching and modulo arithmetic queries
Pattern

$regex — Regular Expression Matching

$regex selects documents where a field value matches a specified regular expression. MongoDB uses Perl Compatible Regular Expressions (PCRE).

Syntax Forms

// Inline regex literal — concise and readable
db.products.find({ name: /^mongo/i })

// Explicit $regex operator form
db.products.find({ name: { $regex: /^mongo/, $options: "i" } })

// String pattern form (useful when building queries dynamically)
db.products.find({ name: { $regex: "^mongo", $options: "i" } })

$options Flags

FlagMeaningUse Case
i Case insensitive User-facing search where casing varies
m Multiline — ^ and $ match start/end of each line Fields containing newline-separated data
s Dotall — . matches newlines as well Matching across line breaks in multiline strings
x Extended — ignores whitespace and allows inline comments Complex patterns that benefit from formatting

Performance: Anchored vs Unanchored Regex

// GOOD: anchored with ^ — can use an index (IXSCAN)
db.products.find({ name: /^Laptop/ })

// BAD: no anchor — always causes a full collection scan (COLLSCAN)
db.products.find({ name: /Laptop/ })

// WORST: leading wildcard — guaranteed COLLSCAN, very slow
db.products.find({ name: /.*Laptop.*/ })
WARN
For full-text search across multiple fields or large bodies of text, use a text index with the $text operator instead of $regex. Text indexes support stemming, stop words, and language-aware matching at far better performance than arbitrary regex.

$mod — Modulo Arithmetic

$mod selects documents where the value of a field divided by a divisor has a specific remainder. Syntax: { field: { $mod: [divisor, remainder] } }.

// Find documents where qty is even (remainder 0 when divided by 2)
db.inventory.find({ qty: { $mod: [2, 0] } })

// Find documents where qty is odd (remainder 1 when divided by 2)
db.inventory.find({ qty: { $mod: [2, 1] } })

// Find items that belong to "bucket 3" in a 5-bucket partition scheme
// Useful for distributing processing across workers
db.jobs.find({ jobId: { $mod: [5, 3] } })

// Find products whose price is a multiple of 50
db.products.find({ price: { $mod: [50, 0] } })
TIP
$mod is useful for data partitioning: assign each worker a remainder value and query only that partition. For example, 4 workers each query $mod: [4, 0], $mod: [4, 1], $mod: [4, 2], and $mod: [4, 3] to divide the collection evenly without overlap.
06
Edge Cases
Traps, type issues, and performance pitfalls
Gotchas

Type Sensitivity

MongoDB is BSON-type sensitive. A query for a number will not match a string that looks like that number, and vice versa. Type mismatches silently return zero results — no error is thrown.

// Document stored as: { age: "25" }  (string)
db.users.find({ age: { $eq: 25 } })   // returns nothing — 25 (int) != "25" (string)
db.users.find({ age: { $eq: "25" } }) // returns the document — type matches

// Always ensure inserted types match your query types
// Use $type operator to diagnose: db.users.find({ age: { $type: "string" } })

$ne and $nin Include Missing Fields

Both $ne and $nin match documents where the target field does not exist. This is consistent with MongoDB's document model but trips up developers coming from SQL where NULL handling is explicit.

// These two documents both match { role: { $ne: "admin" } }
{ _id: 1, name: "Alice", role: "user" }   // "user" != "admin" — match
{ _id: 2, name: "Bob" }                   // no 'role' field — also match!

// Fix: require the field to exist
db.users.find({ role: { $exists: true, $ne: "admin" } })

$in With Empty Array Matches Nothing

// $in with an empty array is a valid query but always returns zero docs
db.products.find({ category: { $in: [] } })
// Result: [] — an empty array has no values to match against

// Guard against this in application code:
if (categories.length > 0) {
  db.products.find({ category: { $in: categories } })
}

$nin With Empty Array Matches Everything

// $nin with an empty array always returns all documents
db.products.find({ category: { $nin: [] } })
// Result: all documents — nothing is excluded when the exclusion set is empty

// This is logical but can be a surprise in dynamic query builders

Regex Performance: Avoid Leading Wildcards

// Index-friendly — anchored, can use IXSCAN
db.products.find({ name: /^Laptop/ })

// NOT index-friendly — unanchored, always COLLSCAN
db.products.find({ name: /Laptop/ })

// AVOID: leading wildcard is the worst case — full COLLSCAN guaranteed
db.products.find({ name: /.*Laptop.*/ })
// On large collections this can be orders of magnitude slower
DANGER
A leading wildcard regex like /.*search_term.*/ will perform a full collection scan regardless of indexes. On a collection with millions of documents this can take seconds and consume significant server resources. Use a text index with $text for substring and full-text matching at scale.

$in Array Size and Performance

There is no hard document limit on the number of values in a $in array, but performance degrades as the array grows. Each value requires a separate index lookup.

// Fine: small set, individual index lookups are fast
db.orders.find({ status: { $in: ["pending", "shipped"] } })

// Problematic: thousands of values — consider refactoring
db.orders.find({ userId: { $in: [/* 50,000 IDs */] } })
// Better approach: store a membership flag on the document itself,
// or use a $lookup join in the aggregation pipeline

Combining $in With $ne

You can compose multiple comparison operators on different fields in a single query object. Fields are ANDed together by default.

// Orders that are in an active status AND NOT placed by an admin
db.orders.find({
  status: { $in: ["pending", "processing"] },
  role:   { $ne: "admin" }
})
// Remember: role: { $ne: "admin" } also matches docs without a 'role' field
// Add $exists: true if that matters for your data model