MongoDB Mastery
MongoDB Mastery lessons / Module 5: Querying Documents

Querying: equality, comparison and logic

Level 1: Beginner20 min

find() with filters: equality, implicit AND, comparison operators, $in, $or, and querying nested fields with dot notation.

This lesson is interactive. Run every example against an in-browser MongoDB, see explain() plans drawn as trees, and take the quiz.
Open the interactive lesson
You will learn to
  • 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.

OperatorMeaningExample
$eq / plain valueequals{ status: "paid" }
$nenot equal (also matches missing){ status: { $ne: "cancelled" } }
$gt $gte $lt $lteranges{ total: { $gte: 100, $lt: 500 } }
$in / $ninany of / none of{ status: { $in: ["paid", "shipped"] } }
$or / $and / $norlogical combinations{ $or: [{ a: 1 }, { b: 2 }] }
$notnegates 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.

Equality and implicit AND
db.orders.find({ status: "paid", "payment.method": "upi" }, { orderNo: 1, total: 1, _id: 0 }).limit(3)
db.orders.countDocuments({ status: "paid", "payment.method": "upi" })
Ranges and $in
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)
$or, $and and $nor
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" }] })
$ne matches missing fields
db.orders.countDocuments({ couponCode: { $ne: "WELCOME10" } })
db.orders.countDocuments({ couponCode: { $exists: true, $ne: "WELCOME10" } })
Most orders have no couponCode at all, and $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

Avoid
db.orders.find({ status: "paid", status: "shipped" })
Prefer
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.

Avoid
db.orders.find({ $or: [
  { status: "paid" }, { status: "shipped" }, { status: "pending" }
] })
Prefer
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 $or on the same field instead of $in; $in is simpler and plans better.
  • Forgetting that $ne and $nin match 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
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.