Skip to content

How to Write a MongoDB Query: A Beginner's Guide

How to write a MongoDB query step by step — find filters, operators like $gt and $in, and building queries visually instead of guessing syntax.

Try it now: Mongo Query Builder Build MongoDB find filters and aggregation pipelines visually, and get them as mongosh, Node driver or PyMongo code — with OR runs always grouped explicitly.

Step 1: Start With the Basic Shape — find({ field: value })

Every MongoDB find query starts from the same shape: db.collection.find({ field: value }). The object you pass is a filter — MongoDB scans (or uses an index on) the collection and returns every document where field equals value exactly. There's no = operator to write; equality is the default when you name a field directly.

a simple equality filter
db.users.find({ status: "active" })

// matches:
{ "_id": ObjectId("..."), "name": "Ada", "status": "active" }
{ "_id": ObjectId("..."), "name": "Grace", "status": "active" }

// does not match:
{ "_id": ObjectId("..."), "name": "Marlon", "status": "inactive" }

An empty filter, db.users.find({}), matches every document in the collection — that's the whole result set, unfiltered. Everything past this point is about narrowing that down, and it's worth knowing that find() always returns a cursor, not an array — .toArray() (in the Node driver or PyMongo) or simply printing it in mongosh is what actually iterates it.

Step 2: Narrow the Match With Comparison Operators

Exact equality only gets you so far. For ranges and exclusions, MongoDB query operators replace the plain value with an object whose key is the operator, prefixed with $:

  • $gt / $gte — greater than / greater than or equal to.
  • $lt / $lte — less than / less than or equal to.
  • $ne — not equal to.
  • $in— matches if the field's value is any of the values in a given array.
  • $nin — the inverse: matches if the value is not in the array.
a range filter with $gte and $lte
db.orders.find({
  total: { $gte: 50, $lte: 200 }
})
// total >= 50 AND total <= 200 — both operators nested under the same field
$in against a list of allowed values
db.orders.find({
  status: { $in: ["shipped", "delivered"] }
})
// matches any document whose status is one of the two listed values

Notice that $gte and $lteabove sit inside the same object, keyed under one field — that's how you express a range on a single field, and it's a different pattern from combining conditions across multiple fields, which is next.

Step 3: Combine Conditions With Implicit AND, $and and $or

When a filter object lists more than one field, MongoDB requires all of them to match — an implicit AND. You don't need to write $and for this, the common case:

implicit AND across two fields
db.orders.find({
  status: "shipped",
  total: { $gt: 100 }
})
// status is "shipped" AND total > 100

You only need the explicit $and operator when you have multiple conditions on the samefield that a plain object can't express as separate keys (a single JavaScript/BSON object can't have two keys with the same name), or when combining full sub-conditions built from $or. $or, in particular, has a strict shape that's worth flagging because it's a common mistake: it takes an array of complete condition objects, not a shorthand list of values. { $or: ["shipped", "delivered"] } is not valid — each array entry must be its own { field: value } object.

$or — an array of full condition objects, and $and combining two $or groups
// correct: $or takes complete { field: value } conditions
db.orders.find({
  $or: [
    { status: "shipped" },
    { total: { $gt: 500 } }
  ]
})
// status is "shipped" OR total > 500

// $and combining two $or groups — needed because each side is itself
// a multi-condition group, not a single field
db.orders.find({
  $and: [
    { $or: [{ status: "shipped" }, { status: "delivered" }] },
    { $or: [{ region: "EU" }, { region: "UK" }] }
  ]
})

If you'd rather not memorize this shape by hand, GenKitLab's Mongo Query Builder builds filters like these visually and always groups $or explicitly, then exports the result as mongosh, Node driver or PyMongo code.

Step 4: Choose Which Fields Come Back — Projection

find()takes a second argument, the projection, that controls which fields are returned per document — it doesn't affect which documents match, only their shape. Set a field to 1 to include it, or 0 to exclude it:

inclusion vs. exclusion projection
// inclusion — return only name and email (plus _id, included by default)
db.users.find({ status: "active" }, { name: 1, email: 1 })

// exclusion — return everything except password
db.users.find({ status: "active" }, { password: 0 })

The one restriction worth knowing before you hit it in practice: you can't mix inclusion and exclusion in the same projection. { name: 1, password: 0 } throws an error — once you include one field explicitly, every other field is implicitly excluded, and mixing in a 0 is contradictory to the engine. The single exception is _id, which you can set to 0 alongside an otherwise inclusion-only projection ({ name: 1, _id: 0 }) since it's included by default and needs its own way to be turned off.

Step 5: Order and Page Through Results — sort, skip, limit

.sort() orders the cursor by one or more fields: 1 for ascending, -1 for descending. Chain .skip() and .limit() onto the same cursor to page through a large result set.

sort, then skip and limit for page 3 of 20-per-page results
db.orders
  .find({ status: "shipped" })
  .sort({ createdAt: -1 })   // newest first
  .skip(40)                 // skip pages 1 and 2 (20 each)
  .limit(20)                // return page 3

Sorting on an unindexed field forces MongoDB to sort in memory, which is fine for small result sets but worth an index once a collection grows — that's a performance concern, not a syntax one, and doesn't change how the query itself is written.

Step 6: Know When find() Isn't Enough — the Aggregation Pipeline

find()filters and shapes individual documents, but it can't group them, compute a running total, or join data across collections. Once a query needs to group by a field and compute a sum or average ($group), reshape or rename fields beyond simple inclusion/exclusion, or pull in related documents from another collection ($lookup, MongoDB's equivalent of a join), the right tool is the aggregation pipeline — db.collection.aggregate([...]), a sequence of stages like $match (a find-style filter), $group and $lookuppiped one into the next. That's a large enough topic to deserve its own tutorial rather than a paragraph bolted onto this one; the honest scope of this guide is find(), and knowing where its limit is is part of writing correct MongoDB queries.

If MongoDB itself is still the open question — whether a document database is the right fit compared to a relational one for what you're building — that comparison, including where each model actually wins, is covered in MongoDB vs. PostgreSQL.

Frequently asked questions

What is the basic syntax of a MongoDB find query?

db.collection.find({ field: value }) — the object is a filter, and naming a field directly checks for exact equality. An empty filter, find({}), matches every document in the collection.

How do I do a range query in MongoDB, like 'greater than' or 'less than'?

Use the comparison operators $gt, $gte, $lt and $lte nested under the field, for example { total: { $gte: 50, $lte: 200 } } for a range, or $ne for not-equal and $in / $nin for matching against a list of allowed or disallowed values.

What's the difference between implicit AND and $and in MongoDB?

Listing multiple fields in one filter object is already an AND — { status: "shipped", total: { $gt: 100 } } requires both to match, with no operator needed. Explicit $and is only necessary when combining full sub-conditions, such as two $or groups, or expressing multiple conditions on the same field.

Why does my $or query in MongoDB not work?

$or requires an array of complete condition objects, each shaped like { field: value } — not a shorthand array of values. { $or: ["shipped", "delivered"] } is invalid; the correct form is { $or: [{ status: "shipped" }, { status: "delivered" }] }.

Can I include and exclude fields in the same MongoDB projection?

No. A projection must be either inclusion-only ({ name: 1, email: 1 }) or exclusion-only ({ password: 0 }) — mixing 1 and 0 across different fields throws an error. The one exception is _id, which can be set to 0 inside an otherwise inclusion-only projection.

How do I sort and paginate MongoDB results?

Chain .sort({ field: 1 }) (1 for ascending, -1 for descending) with .skip() and .limit() on the cursor returned by find() — for example .sort({ createdAt: -1 }).skip(40).limit(20) to get page 3 of 20-per-page results.

When should I use aggregate() instead of find()?

Once a query needs to group documents and compute a sum or average, reshape data beyond simple field inclusion or exclusion, or join in related documents from another collection, find() can't express that — the aggregation pipeline (aggregate(), with stages like $match, $group and $lookup) is the right tool.

Last updated