MongoDB Query Operators: A Practical Guide
A MongoDB query is a document that describes which documents you want back. Operators are the special $-prefixed keys inside that document that express conditions more interesting than plain equality. They are used in find(), in the $match stage of an aggregation pipeline, and anywhere else MongoDB accepts a query filter.
This guide groups the operators by what they do and points out the behaviors that surprise people coming from SQL.
Equality is the default
Before reaching for an operator, remember that a bare field-value pair already means "equals":
db.users.find({ status: "active" })This is identical to db.users.find({ status: { $eq: "active" } }). You only need $eq explicitly in a few edge cases, such as comparing against a value that is itself a document with $-prefixed keys.
One important rule: when the value is an array, equality has two meanings. { tags: "urgent" } matches a document where tags is the scalar "urgent" or an array that contains "urgent". But { tags: ["urgent", "new"] } matches only documents where tags is exactly that array, in that order. Operators behave differently against arrays too, which is the source of most confusion later in this guide.
Comparison operators
These compare a field against a value.
| Operator | Matches when the field is |
|---|---|
$eq | equal to the value |
$ne | not equal to the value |
$gt | greater than |
$gte | greater than or equal |
$lt | less than |
$lte | less than or equal |
$in | equal to any value in an array |
$nin | equal to none of the values in an array |
db.products.find({ price: { $gte: 10, $lte: 50 } })
db.products.find({ category: { $in: ["books", "music"] } })Two things to watch. First, comparisons respect BSON type order, so a query for { age: { $gt: 18 } } will not match documents where age is stored as the string "19". Second, $ne and $nin cannot use an index efficiently to exclude documents, because the index helps you find matches, not absences. They often force a collection scan.
When $gt and friends are applied to an array field, they match if any element satisfies the condition. { scores: { $gt: 90 } } matches a document with scores: [40, 95].
Logical operators
$and, $or, $nor, and $not combine conditions.
db.users.find({
$or: [
{ status: "premium" },
{ signupDate: { $lt: ISODate("2020-01-01") } }
]
})$and is usually implicit. { status: "active", age: { $gt: 21 } } already means both conditions must hold. You only need an explicit $and when you have two conditions on the same field that cannot live in one object, for example two separate $elemMatch clauses, or when mixing operators that would otherwise overwrite each other in a single document.
$not is a prefix that inverts another operator. It does not take a plain value: { price: { $not: { $gt: 100 } } } is valid, but it also matches documents where price does not exist, because "not greater than 100" is true for a missing field.
Element operators
$exists and $type query the shape of a field rather than its value.
db.users.find({ phoneNumber: { $exists: true } })
db.events.find({ timestamp: { $type: "date" } })$exists: true matches documents where the field is present, including those where it is explicitly set to null. If you want present-and-not-null, combine it: { field: { $exists: true, $ne: null } }.
$type matches by BSON type and accepts either the type name ("string", "int", "double", "date", "array") or its numeric alias. It is the practical way to find documents where a field was stored with the wrong type, a common data-quality check.
Array operators
Arrays are where MongoDB queries differ most from relational thinking.
$all requires the array to contain all the listed values, in any order:
db.posts.find({ tags: { $all: ["mongodb", "tutorial"] } })$size matches arrays of an exact length. It does not accept a range, so there is no $size: { $gt: 2 }; for that you query a specific index like { "items.2": { $exists: true } } or use $expr with $size (see below).
$elemMatch is the operator people miss most often. Without it, multiple conditions on an array field are checked against the array as a whole, so each condition can be satisfied by a different element:
// Matches if ANY element is >5 AND ANY (possibly other) element is <10
db.data.find({ readings: { $gt: 5, $lt: 10 } })
// Matches only if a SINGLE element is both >5 and <10
db.data.find({ readings: { $elemMatch: { $gt: 5, $lt: 10 } } })For arrays of subdocuments, $elemMatch is almost always what you want, because it ties the conditions to one element:
db.orders.find({
items: { $elemMatch: { product: "widget", quantity: { $gte: 3 } } }
})Evaluation operators
$regex matches strings against a pattern:
db.users.find({ email: { $regex: /@example\.com$/i } })An anchored, case-sensitive prefix regex like /^abc/ can use an index. A pattern with a leading wildcard or the case-insensitive flag generally cannot, and scans the collection.
$expr lets you use aggregation expressions inside a query, which is the only way to compare two fields of the same document:
db.budgets.find({ $expr: { $gt: ["$spent", "$allocated"] } })Note the $-prefixed field references, which mean "the value of this field" rather than literal strings. $expr is also how you do arithmetic or call functions like $size in a filter. The cost is that $expr conditions are harder for the query planner to optimize, so keep an indexable plain condition alongside it when you can.
$mod matches on a remainder, useful for sampling or bucketing: { id: { $mod: [4, 0] } } matches every fourth id.
Common mistakes
- Forgetting
$elemMatchon subdocument arrays. Conditions get spread across different array elements and you get more matches than expected. - Using
$neor$ninon hot paths. They cannot use indexes to exclude, so they scan. Restructure the query if it is performance-sensitive. - Assuming
$notexcludes missing fields. It does not.$noton a condition also matches documents where the field is absent. - Type mismatches. A numeric range will not match values stored as strings. Clean the data or use
$exprwith a conversion.
Debugging your filters
When a query returns the wrong set, explain("executionStats") shows whether it used an index (IXSCAN) or scanned everything (COLLSCAN), and how many documents it examined versus returned. A large gap between examined and returned usually means a missing or unusable index. For how to build the right index for a filter, see our MongoDB indexes guide, and for filtering inside analytical queries see the aggregation pipeline guide.
Mako connects to MongoDB and can help write and explain these query filters with AI-powered autocomplete while you test which operator and index combination an actual query uses. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.