MongoDB
Logical Operators
Logical operators let you combine multiple filter conditions into a single query. Where comparison operators ask "is this field greater than X?", logical operators ask "does this document satisfy condition A and condition B?" or "does it satisfy A or B?". MongoDB provides four logical operators: $and, $or, $not, and $nor. Each takes an array of conditions (except $not, which wraps a single operator expression) and evaluates them together to produce a true or false result for each document. Understanding when to use each operator — and when the implicit AND shorthand is sufficient — is essential for writing clean, correct, and performant queries.
Implicit AND — The Default Behaviour
When you place multiple fields in a single filter document, MongoDB combines them with an implicit AND — every condition must be true for a document to match. This is the most common form of multi-condition query and requires no explicit operator.
Why it exists: the shorthand keeps simple queries concise. You only need explicit $and when the shorthand cannot express your intent — specifically when you need two conditions on the same field.
# Implicit AND — multiple fields in one filter dict
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# All three conditions must be true
# Equivalent to: membership == "premium" AND country == "UK" AND age >= 25
uk_premium_25 = db.users.find(
{
"membership": "premium",
"country": "UK",
"age": {"$gte": 25}
},
{"name": 1, "membership": 1, "country": 1, "age": 1, "_id": 0}
)
print("Premium UK users aged 25+ (implicit AND):")
for u in uk_premium_25:
print(f" {u['name']} — {u['country']}, age {u['age']}, {u['membership']}")
# Electronics products under $100
affordable_electronics = db.products.find(
{"category": "Electronics", "price": {"$lt": 100}},
{"name": 1, "price": 1, "_id": 0}
)
print("\nElectronics under $100 (implicit AND):")
for p in affordable_electronics:
print(f" ${p['price']:>6.2f} {p['name']}")Alice Johnson — UK, age 30, premium
Electronics under $100 (implicit AND):
$29.99 Wireless Mouse
$49.99 USB-C Hub
$89.99 Mechanical Keyboard
- Implicit AND is always preferred over explicit
$andwhen conditions are on different fields — it is shorter and equally performant - A filter dict can have as many fields as needed — all must match for a document to be included
- The implicit AND does not work when you need two separate conditions on the same field name — a dict key can only appear once, so the second value silently overwrites the first
$and — Explicit AND
Explicit $and takes an array of condition objects and requires every one to be true. Its primary use case is when you need two or more conditions on the same field — something the implicit shorthand cannot express. It is also useful for clarity when combining complex nested conditions.
# $and — required when two conditions target the same field
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# WHY explicit $and is needed here:
# This SILENTLY FAILS — second "price" key overwrites the first
broken_range = {"price": {"$gte": 30}, "price": {"$lte": 100}}
# Python dict deduplicates keys — only {"price": {"$lte": 100}} survives
print("Broken range (duplicate key problem):")
print(f" Dict kept: {broken_range}") # only $lte remains
# CORRECT — use $and to apply two conditions to the same field
# (Normally a range on one field is written as one dict — this demo shows $and)
explicit_range = db.products.find(
{"$and": [
{"price": {"$gte": 30}},
{"price": {"$lte": 100}}
]},
{"name": 1, "price": 1, "_id": 0}
)
print("\nProducts $30–$100 (explicit $and):")
for p in explicit_range:
print(f" ${p['price']:>6.2f} {p['name']}")
# Practical use — combine $and with $or for complex logic
# Electronics that are either highly rated OR very cheap
result = db.products.find(
{"$and": [
{"category": "Electronics"},
{"$or": [
{"rating": {"$gte": 4.6}},
{"price": {"$lt": 35}}
]}
]},
{"name": 1, "category": 1, "price": 1, "rating": 1, "_id": 0}
)
print("\nElectronics with rating >= 4.6 OR price < $35:")
for p in result:
print(f" {p['name']:25} ${p['price']:>7.2f} rating: {p['rating']}")Dict kept: {'price': {'$lte': 100}}
Products $30–$100 (explicit $and):
$ 49.99 USB-C Hub
$ 89.99 Mechanical Keyboard
Electronics with rating >= 4.6 OR price < $35:
Wireless Mouse $ 29.99 rating: 4.5
Mechanical Keyboard $ 89.99 rating: 4.7
Monitor 27-inch $299.99 rating: 4.6
- The duplicate key bug is a common Python mistake — always use explicit
$andor combine both operators in a single value dict ({"price": {"$gte": 30, "$lte": 100}}) for range queries - Explicit
$andis useful for readability when combining complex nested logical expressions - MongoDB's query optimiser treats explicit and implicit AND identically — there is no performance difference
$or — At Least One Condition Must Match
$or takes an array of conditions and returns documents where at least one condition is true. It is the right tool when documents may qualify through different criteria — for example, "show me products that are on sale OR have a high rating".
# $or — at least one condition must be true
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# Products that are either in Furniture OR cost less than $5
budget_or_furniture = db.products.find(
{"$or": [
{"category": "Furniture"},
{"price": {"$lt": 5}}
]},
{"name": 1, "category": 1, "price": 1, "_id": 0}
)
print("Furniture OR under $5:")
for p in budget_or_furniture:
print(f" {p['name']:28} ${p['price']:>7.2f} ({p['category']})")
# Users from UK OR aged under 29
print("\nUsers from UK OR under 29:")
result = db.users.find(
{"$or": [
{"country": "UK"},
{"age": {"$lt": 29}}
]},
{"name": 1, "country": 1, "age": 1, "_id": 0}
)
for u in result:
print(f" {u['name']:15} {u['country']:8} age {u['age']}")
# $or vs $in — prefer $in when all conditions test the same field for equality
# These two are equivalent — $in is more concise and slightly more efficient:
using_or = {"$or": [{"country": "UK"}, {"country": "Spain"}, {"country": "Germany"}]}
using_in = {"country": {"$in": ["UK", "Spain", "Germany"]}}
print("\n$or vs $in — results should be identical:")
or_count = db.users.count_documents(using_or)
in_count = db.users.count_documents(using_in)
print(f" $or count: {or_count} | $in count: {in_count} | Same: {or_count == in_count}")Standing Desk $349.99 (Furniture)
Notebook A5 $ 4.99 (Stationery)
Ballpoint Pens 10-pack $ 3.49 (Stationery)
Users from UK OR under 29:
Alice Johnson UK age 30
Bob Smith UK age 25
Clara Diaz Spain age 28
$or vs $in — results should be identical:
$or count: 3 | $in count: 3 | Same: True
- When all
$orconditions test the same field for equality, replace them with$in— it is more concise and MongoDB can use a single index scan $orwith conditions on different fields may require multiple index scans — one per branch — which MongoDB's query planner handles automatically- A document is returned once even if it satisfies multiple
$orbranches — no duplicates
$nor — All Conditions Must Be False
$nor is the logical complement of $or — it returns documents where every condition in the array is false. It also matches documents where the tested fields do not exist. Think of it as "none of the above".
# $nor — none of the conditions may be true
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# Orders that are neither cancelled NOR processing NOR shipped
clean_orders = db.orders.find(
{"$nor": [
{"status": "cancelled"},
{"status": "processing"},
{"status": "shipped"}
]},
{"_id": 1, "status": 1, "total": 1}
)
print("Orders that are not cancelled, processing, or shipped:")
for o in clean_orders:
print(f" {o['_id']} {o['status']:12} ${o['total']:.2f}")
# Products that are NOT Electronics AND NOT Furniture
# (only Stationery remains)
non_tech = db.products.find(
{"$nor": [
{"category": "Electronics"},
{"category": "Furniture"}
]},
{"name": 1, "category": 1, "_id": 0}
)
print("\nProducts not in Electronics or Furniture:")
for p in non_tech:
print(f" {p['name']} ({p['category']})")
# $nor also matches documents missing the field entirely
# This finds users who are neither premium NOR have a membership field
result = db.users.find(
{"$nor": [{"membership": "premium"}]},
{"name": 1, "membership": 1, "_id": 0}
)
print("\nUsers who are not premium (including missing field):")
for u in result:
print(f" {u['name']} — {u.get('membership', '[no field]')}")o001 delivered $44.96
o003 delivered $99.98
o005 delivered $329.98
o007 delivered $11.97
Products not in Electronics or Furniture:
Notebook A5 (Stationery)
Ballpoint Pens 10-pack (Stationery)
Users who are not premium (including missing field):
Bob Smith — basic
David Lee — basic
$norlike$nealso matches documents where the field is absent — keep this in mind to avoid unexpected results$norwith a single condition is equivalent to$notapplied to the whole condition- For simple "not equal to any of these values" queries,
$ninis more concise than$nor
$not — Inverting a Single Condition
$not inverts the result of a single operator expression applied to one field. Unlike $nor and $or which sit at the top level of a filter, $not is applied to a field's value and must wrap an operator expression — it cannot be used with a plain equality value.
# $not — invert a single operator expression on a field
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# Products where price is NOT less than $50
# Equivalent to: price >= $50
not_cheap = db.products.find(
{"price": {"$not": {"$lt": 50}}},
{"name": 1, "price": 1, "_id": 0}
)
print("Products where price is NOT < $50:")
for p in not_cheap:
print(f" ${p['price']:>7.2f} {p['name']}")
# Users where age is NOT between 25 and 35
outside_range = db.users.find(
{"age": {"$not": {"$gte": 25, "$lte": 35}}},
{"name": 1, "age": 1, "_id": 0}
)
print("\nUsers where age is NOT between 25 and 35:")
for u in outside_range:
print(f" {u['name']} — age {u['age']}")
# $not with $regex — names that do NOT start with a vowel
no_vowel_start = db.users.find(
{"name": {"$not": {"$regex": "^[AEIOUaeiou}"}}},
{"name": 1, "_id": 0}
)
print("\nUsers whose name does NOT start with a vowel:")
for u in no_vowel_start:
print(f" {u['name']}")
# Common mistake — $not cannot wrap a plain value
# WRONG: {"membership": {"$not": "premium"}} ← raises an error
# RIGHT: {"membership": {"$not": {"$eq": "premium"}}}
# BETTER: {"membership": {"$ne": "premium"}} ← simplest approach
print("\n$not vs $ne — both find non-premium users:")
not_form = db.users.count_documents({"membership": {"$not": {"$eq": "premium"}}})
ne_form = db.users.count_documents({"membership": {"$ne": "premium"}})
print(f" $not form: {not_form} | $ne form: {ne_form} | Same: {not_form == ne_form}")$ 89.99 Mechanical Keyboard
$299.99 Monitor 27-inch
$349.99 Standing Desk
$ 49.99 USB-C Hub
Users where age is NOT between 25 and 35:
Eva Müller — age 40
Users whose name does NOT start with a vowel:
Bob Smith
Clara Diaz
David Lee
$not vs $ne — both find non-premium users:
$not form: 2 | $ne form: 2 | Same: True
$notmust wrap an operator expression —{"field": {"$not": "value"}}raises an error; use$nefor simple inequality instead$notalso matches documents where the field does not exist — same behaviour as$neand$nor- Prefer
$neover{"$not": {"$eq": ...}}— it is more readable and has identical performance $notshines when inverting complex expressions that have no simple inverse operator — like inverting a regex or a range
Combining Logical Operators
Logical operators compose freely — you can nest $or inside $and, combine $not with ranges, and build arbitrarily complex query trees. The key is keeping the structure readable.
# Combining logical operators — complex query compositions
from pymongo import MongoClient
client = MongoClient("mongodb://localhost:27017/")
db = client["dataplexa"]
# Find orders that:
# - belong to a premium user (u001, u003, u005) AND
# - are either delivered OR have a total above $200
complex_query = db.orders.find(
{
"$and": [
{"user_id": {"$in": ["u001", "u003", "u005"]}},
{"$or": [
{"status": "delivered"},
{"total": {"$gt": 200}}
]}
]
},
{"_id": 1, "user_id": 1, "status": 1, "total": 1}
)
print("Premium user orders — delivered OR total > $200:")
for o in complex_query:
print(f" {o['_id']} user: {o['user_id']} {o['status']:12} ${o['total']:.2f}")
# Products that are:
# - NOT Stationery AND
# - (price > $200 OR rating >= 4.7)
premium_products = db.products.find(
{
"category": {"$ne": "Stationery"},
"$or": [
{"price": {"$gt": 200}},
{"rating": {"$gte": 4.7}}
]
},
{"name": 1, "category": 1, "price": 1, "rating": 1, "_id": 0}
)
print("\nNon-stationery products: price > $200 OR rating >= 4.7:")
for p in premium_products:
print(f" {p['name']:25} ${p['price']:>7.2f} rating {p['rating']}")o001 user: u001 delivered $44.96
o003 user: u001 delivered $99.98
o004 user: u003 processing $349.99
o005 user: u004 delivered $329.98
Non-stationery products: price > $200 OR rating >= 4.7:
Mechanical Keyboard $ 89.99 rating 4.7
Standing Desk $349.99 rating 4.8
Monitor 27-inch $299.99 rating 4.6
- Mix implicit AND (comma-separated fields) with explicit
$orfreely — they compose cleanly in a single filter object - Deep nesting makes queries hard to read and debug — if a query has more than three levels of nesting, consider refactoring your data model
- MongoDB's query planner evaluates logical operators left to right and short-circuits where possible — put the most selective condition first
Summary Table
| Operator | Syntax | Matches When | Key Note |
|---|---|---|---|
| Implicit AND | {f1: v1, f2: v2} |
All conditions true | Use for conditions on different fields |
$and |
{"$and": [{...},{...}]} |
All conditions true | Required for two conditions on same field |
$or |
{"$or": [{...},{...}]} |
At least one condition true | Use $in when all branches test same field |
$nor |
{"$nor": [{...},{...}]} |
All conditions false | Also matches missing fields |
$not |
{"field": {"$not": {op}}} |
Condition on field is false | Must wrap an operator — use $ne for simple inequality |
Practice Questions
Practice 1. Why does the implicit AND shorthand fail when you need two conditions on the same field?
Practice 2. Write a filter to find all orders that are either shipped or have a total greater than $300.
Practice 3. What is the difference between $nor and $not?
Practice 4. When should you prefer $in over $or for multi-value matching?
Practice 5. Write the correct $not filter to find products where rating is NOT between 4.0 and 4.5 inclusive.
Quiz
Quiz 1. When is explicit $and required instead of the implicit AND shorthand?
Quiz 2. What does $nor match in addition to documents where all conditions are false?
Quiz 3. Why does {"membership": {"$not": "premium"}} raise an error?
Quiz 4. If a document satisfies two branches of a $or query, how many times does it appear in the results?
Quiz 5. Which logical operator is most useful when you want to invert a complex regex or range expression on a single field?
Next up — Array Queries: Querying into arrays with $all, $elemMatch, $size, and positional operators to find and filter embedded data.