← MongoDB Notes

Logical
Operators

FILE 09_logical_operators
TOPIC $and · $or · $not · $nor · Implicit AND · Performance
LEVEL Foundation
01
Overview
All four logical operators at a glance
intro

Logical operators combine or invert query conditions. They operate on condition objects, not on raw values. All four are prefixed with $ and can be used inside a filter document passed to find(), updateMany(), deleteMany(), and aggregation $match.

OperatorCondition for matchAccepts
$andALL conditions in the array must be trueArray of condition objects
$orAT LEAST ONE condition must be trueArray of condition objects
$notThe single operator expression must be falseSingle operator expression (not an array)
$norNONE of the conditions must be trueArray of condition objects

General Syntax for Each

// $and — all must be true
db.col.find({ $and: [ { field1: val1 }, { field2: { $gt: val2 } } ] })

// $or — at least one must be true
db.col.find({ $or: [ { status: "active" }, { role: "admin" } ] })

// $not — inverts a single operator expression on one field
db.col.find({ price: { $not: { $gt: 100 } } })

// $nor — none must be true
db.col.find({ $nor: [ { status: "banned" }, { verified: false } ] })
NOTE
$and, $or, $nor all accept an array of condition objects. $not is different — it wraps a single operator expression applied to one specific field and is NOT a top-level operator.
02
$and
Explicit AND · Implicit AND · Duplicate-key trap
and

Explicit $and

The explicit form wraps all conditions in an array. Every single condition must evaluate to true for a document to be returned.

// Explicit $and — both conditions required
db.products.find({
  $and: [
    { category: "electronics" },
    { stock: { $gt: 0 } }
  ]
})

Implicit AND (Comma-Separated Fields)

When multiple fields are listed inside a single query object, MongoDB automatically applies an implicit AND. The two forms below are functionally identical when conditions target different fields.

// Implicit AND — most common form for different fields
db.products.find({ category: "electronics", stock: { $gt: 0 } })

// Exactly equivalent to:
db.products.find({
  $and: [
    { category: "electronics" },
    { stock: { $gt: 0 } }
  ]
})

CRITICAL: When You MUST Use Explicit $and

JavaScript objects cannot have duplicate keys. If you apply two conditions to the same field as separate keys, the second silently overwrites the first. This is one of the most common MongoDB bugs.

BUG
The query below looks like it filters price between 100 and 500, but the second price key overwrites the first in the JS object. MongoDB only ever receives { price: { $lt: 500 } } — losing the lower bound entirely.
// WRONG — second price key silently overwrites first
db.products.find({
  price: { $gt: 100 },   // this gets overwritten by the line below
  price: { $lt: 500 }    // only this condition reaches MongoDB
})
// Effective query sent to MongoDB: { price: { $lt: 500 } }
// CORRECT option A — explicit $and prevents the duplicate-key problem
db.products.find({
  $and: [
    { price: { $gt: 100 } },
    { price: { $lt: 500 } }
  ]
})

// CORRECT option B — range shorthand (idiomatic and preferred)
db.products.find({ price: { $gt: 100, $lt: 500 } })
TIP
The shorthand { price: { $gt: 100, $lt: 500 } } is idiomatic MongoDB for range queries and is preferred over explicit $and when both conditions target the same field. Use explicit $and when you need multiple conditions across different fields that cannot use the implicit comma form.

Index Strategy with $and

MongoDB can satisfy multiple AND conditions efficiently with a compound index. An index on { category: 1, stock: 1 } allows a single tight index scan for a query that filters by both fields.

// Create a compound index to support this $and query
db.products.createIndex({ category: 1, stock: 1 })

// This query now does a single index scan — no collection scan
db.products.find({ category: "electronics", stock: { $gt: 0 } })
03
$or
Union logic · $or vs $in · Index union performance
or

$or Syntax

At least one condition in the array must match for a document to be returned. Conditions can span different fields or apply different operators to the same field.

// Match users where city is "Mumbai" OR "Delhi"
db.users.find({
  $or: [
    { city: "Mumbai" },
    { city: "Delhi" }
  ]
})

// Match users who are admin OR under 18
db.users.find({
  $or: [
    { role: "admin" },
    { age: { $lt: 18 } }
  ]
})

$or vs $in — Same Field, Multiple Values

When you need to match a single field against multiple possible values, $in is more efficient and cleaner than $or. MongoDB can resolve $in with a single index scan rather than the multi-branch index union that $or requires.

// $or on the same field — works but not optimal
db.users.find({
  $or: [
    { city: "Mumbai" },
    { city: "Delhi" },
    { city: "Bangalore" }
  ]
})

// $in on same field — equivalent, cleaner, and more efficient
db.users.find({ city: { $in: ["Mumbai", "Delhi", "Bangalore"] } })

// $or is necessary when branches span DIFFERENT fields
db.users.find({
  $or: [
    { city: "Mumbai" },
    { role: "admin" }    // different field — $in cannot do this
  ]
})

Performance: Index Union for $or Branches

MongoDB achieves best performance when each branch of an $or has its own supporting index. MongoDB will perform an index union, scanning each index independently and merging the result sets — avoiding a full collection scan.

// Create a separate index for EACH $or branch
db.users.createIndex({ city: 1 })
db.users.createIndex({ role: 1 })

// MongoDB can now use index union for this query
db.users.find({
  $or: [
    { city: "Mumbai" },   // satisfied by city index
    { role: "admin" }     // satisfied by role index
  ]
})

// Verify index usage with explain()
db.users.find({ $or: [{ city: "Mumbai" }, { role: "admin" }] })
        .explain("executionStats")
WARN
If even one branch of an $or lacks an index, MongoDB may fall back to a full collection scan for the entire query. Ensure every branch has its own index on large collections.

Nested $or Inside $and

// Verified users who are in Mumbai OR have admin role
db.users.find({
  $and: [
    { verified: true },
    {
      $or: [
        { city: "Mumbai" },
        { role: "admin" }
      ]
    }
  ]
})

// Implicit AND shorthand — equivalent and cleaner
db.users.find({
  verified: true,
  $or: [
    { city: "Mumbai" },
    { role: "admin" }
  ]
})
04
$not
Single-expression inversion · Regex negation · $not vs $ne
not

$not Syntax

$not inverts the result of a single operator expression applied to a single field. It is NOT a top-level logical operator that accepts an array — it targets one field and wraps one operator expression inside it.

// $not structure — wraps ONE operator expression on ONE field
{ field: { $not: { $operator: value } } }

// Match products where price is NOT greater than 100
db.products.find({ price: { $not: { $gt: 100 } } })

// This returns: price <= 100 AND docs where price field doesn't exist

$not with Regex

$not also accepts a regular expression directly without wrapping it in an operator object — one of the few exceptions to the "must wrap an operator" rule.

// Match users whose name does NOT start with "Raj"
db.users.find({ name: { $not: /^Raj/ } })

// Equivalent using $regex operator form
db.users.find({ name: { $not: { $regex: "^Raj" } } })

// Negate email domain match
db.users.find({ email: { $not: { $regex: "@tempmail\\.com$" } } })

CRITICAL: $not Does NOT Accept Plain Values

ERROR
{ field: { $not: value } } is invalid and throws a BadValue: $not needs a regex or a document error. $not must wrap an operator expression or a regex — never a plain value.
// WRONG — $not cannot take a plain value
db.users.find({ status: { $not: "banned" } })
// MongoServerError: BadValue: $not needs a regex or a document

// CORRECT — use $ne for plain value inequality
db.users.find({ status: { $ne: "banned" } })

// CORRECT — use $not with $eq if you want the $not form
db.users.find({ status: { $not: { $eq: "banned" } } })

$not Returns Documents with Missing Fields Too

$not matches documents where the condition is false, which includes documents where the field does not exist at all. This mirrors the behaviour of $ne.

// All three types of documents match this query:
db.products.find({ price: { $not: { $gt: 100 } } })
// 1. { price: 50 }    ← price is not > 100 ✓
// 2. { price: 0 }     ← price is not > 100 ✓
// 3. { name: "cap" }  ← no price field at all — ALSO MATCHES ✓

// To exclude missing fields, combine with $exists
db.products.find({
  price: { $exists: true, $not: { $gt: 100 } }
})

$not vs $ne — Which to Use

OperatorUse whenAccepts
$notInverting any operator expression (ranges, regex, etc.)Operator expression or regex literal
$neSimple value inequality — field must not equal valueA plain value
// $ne — idiomatic for simple inequality
db.users.find({ status: { $ne: "banned" } })

// $not with $eq — same effect but verbose; avoid unless needed
db.users.find({ status: { $not: { $eq: "banned" } } })

// $not is the only option when inverting complex operators
db.users.find({ score: { $not: { $gte: 50, $lte: 80 } } })
05
$nor
None-match · Exclusion lists · Missing field behaviour
nor

$nor Syntax

$nor is the logical complement of $or. A document matches only if none of the conditions in the array are true. Logically equivalent to NOT (cond1 OR cond2 OR ...).

// Match users who are neither "banned" nor "suspended"
db.users.find({
  $nor: [
    { status: "banned" },
    { status: "suspended" }
  ]
})

Exclusion Lists Across Multiple Fields

$nor is ideal for blacklist filtering across different fields in a single clean expression — replacing what would otherwise require several chained $ne conditions.

// Exclude documents matching any of these conditions
db.products.find({
  $nor: [
    { status: "discontinued" },
    { stock: 0 },
    { category: "recalled" }
  ]
})

// Equivalent without $nor — more verbose
db.products.find({
  status:   { $ne: "discontinued" },
  stock:    { $ne: 0 },
  category: { $ne: "recalled" }
})

$nor Matches Documents Where All Listed Fields are Missing

Like $not, $nor returns documents where the listed fields are completely absent because none of the conditions can evaluate to true on a document that lacks those fields.

// $nor returns docs where ALL specified conditions are false
db.users.find({
  $nor: [
    { status: "banned" },
    { role: "guest" }
  ]
})
// Matches:
// { name: "Alice", status: "active", role: "user" }  ✓
// { name: "Bob" }   ← has NEITHER field — also matches! ✓

// Fix: explicitly require fields to exist
db.users.find({
  status: { $exists: true },
  role:   { $exists: true },
  $nor: [
    { status: "banned" },
    { role: "guest" }
  ]
})

$nor vs $not

OperatorOperates onBest for
$norArray of multiple condition objectsBlacklist filtering across multiple fields
$notSingle operator expression on one fieldInverting one specific operator condition
// $nor — exclude a combination of statuses and priorities
db.orders.find({
  $nor: [
    { status: "cancelled" },
    { status: "refunded" },
    { priority: "low" }
  ]
})

// $not — invert a single field's range condition
db.orders.find({ amount: { $not: { $lt: 50 } } })
06
Edge Cases
Common bugs · Gotchas · Nested operators · Safe patterns
gotchas

Gotcha 1: Implicit AND Silent Key Overwrite

The most common MongoDB beginner bug. Two conditions targeting the same field as separate JS object keys — the second silently overwrites the first before the query ever reaches MongoDB.

// BUG — only { price: { $lt:500 } } reaches MongoDB
db.products.find({ price: { $gt: 100 }, price: { $lt: 500 } })

// FIX A — explicit $and avoids duplicate key collision
db.products.find({
  $and: [{ price: { $gt: 100 } }, { price: { $lt: 500 } }]
})

// FIX B — range shorthand (preferred idiomatic style)
db.products.find({ price: { $gt: 100, $lt: 500 } })

Gotcha 2: $not with a Plain Value

// ERROR — $not wraps operators, not plain values
db.users.find({ status: { $not: "active" } })
// MongoServerError: $not needs a regex or a document

// FIX — use $ne for plain value negation
db.users.find({ status: { $ne: "active" } })

Gotcha 3: $or Index Union — Each Branch Needs Its Own Index

// Without indexes on both fields, MongoDB does a full collection scan
db.users.find({ $or: [{ city: "Mumbai" }, { role: "admin" }] })

// Solution: index every $or branch independently
db.users.createIndex({ city: 1 })
db.users.createIndex({ role: 1 })

// Confirm index union is being used
db.users.find({ $or: [{ city: "Mumbai" }, { role: "admin" }] })
        .explain("executionStats")
// Look for inputStages with IXSCAN in the winningPlan output

Gotcha 4: Empty Arrays Throw Errors

ERROR
Passing an empty array to $and, $or, or $nor throws a BadValue error. Always ensure at least one condition exists before querying.
// All three throw BadValue errors
db.col.find({ $and: [] })
db.col.find({ $or:  [] })
db.col.find({ $nor: [] })

// Safe pattern — build conditions dynamically
const conditions = []
if (filterCity)   conditions.push({ city: filterCity })
if (filterRole)   conditions.push({ role: filterRole })

const query = conditions.length > 0
  ? { $or: conditions }
  : {}

db.users.find(query)

Gotcha 5: $nor Includes Docs with Absent Fields

// $nor matches docs missing the listed fields entirely
db.users.find({ $nor: [{ status: "banned" }] })
// Returns { _id: 1, status: "active" }         ✓ expected
// Also returns { _id: 2, name: "Bob" }           ✓ but surprising!
// Because: { status: "banned" } is false on a doc with no status field

// Safe pattern: require field existence alongside $nor
db.users.find({
  status: { $exists: true },
  $nor: [{ status: "banned" }]
})

Nested Operators: $and with $or Inside

// Verified users with 30+ days, who are admin OR in Mumbai
db.users.find({
  $and: [
    { verified: true },
    { accountAge: { $gte: 30 } },
    {
      $or: [
        { role: "admin" },
        { city: "Mumbai" }
      ]
    }
  ]
})

// Shorthand using implicit AND for non-conflicting top-level keys
db.users.find({
  verified: true,
  accountAge: { $gte: 30 },
  $or: [
    { role: "admin" },
    { city: "Mumbai" }
  ]
})

// TWO $or groups at top level — must use explicit $and
db.users.find({
  $and: [
    { $or: [{ city: "Mumbai" }, { city: "Delhi" }] },
    { $or: [{ role: "admin" }, { role: "manager" }] }
  ]
})
TIP
You can only have one $or key at the top level of a filter object (JS key uniqueness). To combine two separate $or groups, wrap them in an explicit $and: [ {$or:[...]}, {$or:[...]} ].