Querying: equality, comparison and logic
find() with filters: equality, implicit AND, comparison operators, $in, $or, and querying nested fields with dot notation.
- Write equality and range filters
- Combine conditions with implicit AND, $and, $or, $nor, $not
- Use $in and $nin
- Query embedded fields with dot notation
conceptThe concept
A filter is a document. Each field condition must match; listing several fields is an implicit AND.
| Operator | Meaning | Example |
|---|---|---|
$eq / plain value | equals | { status: "paid" } |
$ne | not equal (also matches missing) | { status: { $ne: "cancelled" } } |
$gt $gte $lt $lte | ranges | { total: { $gte: 100, $lt: 500 } } |
$in / $nin | any of / none of | { status: { $in: ["paid", "shipped"] } } |
$or / $and / $nor | logical combinations | { $or: [{ a: 1 }, { b: 2 }] } |
$not | negates an operator expression | { total: { $not: { $gt: 100 } } } |
Use dot notation for embedded fields: { "shipping.city": "Pune" }. Matching a whole embedded document ({ shipping: { city: "Pune", country: "India" } }) requires an exact match including field order, so prefer dot notation.
You need explicit $and only when the same field or operator appears twice, for example two $or clauses.
whyWhy it matters
Every read in your application is one of these filters. Writing them precisely affects correctness ($ne also matches documents where the field is missing) and performance (equality on indexed fields is the cheapest possible predicate).
Use caseReal-world scenario
Customer support searches for orders that are paid or shipped, over 1,000, shipped to Mumbai: { status: { $in: ["paid", "shipped"] }, total: { $gt: 1000 }, "shipping.city": "Mumbai" }.
Try itExamples
Run and edit these in the interactive lesson.
db.orders.find({ status: "paid", "payment.method": "upi" }, { orderNo: 1, total: 1, _id: 0 }).limit(3)
db.orders.countDocuments({ status: "paid", "payment.method": "upi" })db.orders.countDocuments({ total: { $gte: 100, $lt: 200 } })
db.orders.countDocuments({ status: { $in: ["paid", "shipped"] } })
db.customers.find({ tier: { $in: ["gold", "platinum"] }, "address.country": "UK" }, { name: 1, tier: 1 }).limit(5)db.orders.countDocuments({ $or: [{ status: "returned" }, { total: { $gt: 3000 } }] })
db.orders.countDocuments({
$and: [
{ $or: [{ status: "delivered" }, { status: "shipped" }] },
{ $or: [{ "payment.method": "cod" }, { couponCode: { $exists: true } }] }
]
})
db.orders.countDocuments({ $nor: [{ status: "cancelled" }, { status: "returned" }] })db.orders.countDocuments({ couponCode: { $ne: "WELCOME10" } })
db.orders.countDocuments({ couponCode: { $exists: true, $ne: "WELCOME10" } })$ne counts them too.internalsUnder the hood
The server parses the filter into a match expression tree and normalizes it (for example, $in with one value becomes equality, nested $and are flattened). The query planner then looks for indexes whose prefix matches equality and range predicates. $or can use a different index per branch (an OR stage). Predicates like $ne, $nin and $not are poorly selective and rarely benefit from an index.
Avoid / preferDo this, not that
db.orders.find({ status: "paid", status: "shipped" })db.orders.find({ status: { $in: ["paid", "shipped"] } })A JavaScript object cannot hold the same key twice: the second status silently replaces the first, so the query only finds shipped orders.
db.orders.find({ $or: [
{ status: "paid" }, { status: "shipped" }, { status: "pending" }
] })db.orders.find({ status: { $in: ["paid", "shipped", "pending"] } })For several values of one field, $in is shorter and becomes a single index scan with several bounds; $or plans each clause separately.
PitfallsCommon mistakes
- Writing
{ total: { $gt: 100 }, total: { $lt: 500 } }. In JavaScript the second key overwrites the first; combine them:{ total: { $gt: 100, $lt: 500 } }. - Matching embedded documents by full value when you meant dot notation.
- Using
$oron the same field instead of$in;$inis simpler and plans better. - Forgetting that
$neand$ninmatch documents where the field does not exist.
LimitsWhen not to use it
Equality and range filters are cheap only when an index backs them; without one every find reads the whole collection. Do not use countDocuments() on a huge collection just to show a badge: estimatedDocumentCount() reads metadata instead of scanning.
SecuritySecurity notes
For values that come from users, write { email: { $eq: input } } rather than { email: input }. With $eq, an attacker who sends an object such as { "$ne": "" } gets a literal comparison instead of an operator.
ChallengeTry it yourself
Count delivered or shipped orders over 500 that were paid by card or UPI.
Show one solution
db.orders.countDocuments({
status: { $in: ["delivered", "shipped"] },
total: { $gt: 500 },
"payment.method": { $in: ["card", "upi"] }
})InterviewQuestions you should be able to answer
When do you need an explicit $and?
When the same field or the same operator must appear more than once in one filter, for example two separate $or groups, or two $expr conditions. Otherwise listing conditions side by side is already an AND.
What is the difference between {a: {b: 1}} and {"a.b": 1}?
The first matches only documents where a is exactly { b: 1 } with no other fields and the same field order. The second matches any document whose a.b equals 1, regardless of other fields in a.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.