MongoDB Cheatsheet
Query Operators
Use this MongoDB reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Comparison
A query document is a set of field conditions. Every operator is a key inside the
value, so { qty: { $gt: 5 } } and never { $gt: { qty: 5 } }.
| Operator | Matches | Example |
|---|---|---|
$eq | Equal. Implied when you write a bare value | { qty: { $eq: 5 } } |
$ne | Not equal. Also matches documents missing the field | { status: { $ne: "paid" } } |
$gt $gte | Greater than, greater or equal | { qty: { $gte: 10 } } |
$lt $lte | Less than, less or equal | { placed_at: { $lt: ISODate("2026-01-01") } } |
$in | Equal to any value in the array | { status: { $in: ["paid", "shipped"] } } |
$nin | Equal to none of them, missing field included | { status: { $nin: ["cancelled"] } } |
Two conditions on one field combine, no $and needed:
db.orders.find({ qty: { $gte: 5, $lt: 20 } }) db.orders.find({ placed_at: { $gte: ISODate("2026-01-01"), $lt: ISODate("2026-02-01") } })
Comparisons respect the BSON type order, and they only match within a type bracket.
A string "5" is never $gt: 4, and numeric types (int, long, double, decimal) do
compare against each other. $ne and $nin are negations, so the planner cannot seek
with them and usually scans the whole index.
Logical
Top-level keys are an implicit and. Reach for $and only when the same operator
key would repeat, which in practice means two $or blocks.
| Operator | Form | Notes |
|---|---|---|
$and | { $and: [ {...}, {...} ] } | Rarely needed. Required to combine two $ors |
$or | { $or: [ {...}, {...} ] } | Each branch should have its own index, or the whole thing scans |
$not | { f: { $not: { $gt: 5 } } } | Wraps an operator expression, not a field condition |
$nor | { $nor: [ {...}, {...} ] } | Matches documents failing every clause |
// Implicit and db.orders.find({ status: "paid", "items.sku": "A-1" }) // Two independent or-groups need $and db.orders.find({ $and: [ { $or: [ { status: "paid" }, { status: "shipped" } ] }, { $or: [ { region: "EU" }, { region: "UK" } ] } ] })
$not also negates a missing field, so { qty: { $not: { $gt: 5 } } } matches
documents with no qty at all. If you mean "present and not greater than 5", add
$exists: true.
Element and evaluation
| Operator | Matches |
|---|---|
$exists: true | Field is present, even when its value is null |
$exists: false | Field is absent |
$type: "string" | BSON type by alias or number. Aliases: double string object array binData objectId bool date null regex int long decimal |
$type: "number" | Alias covering every numeric type |
$type: ["string", "int"] | Any of several types |
$regex | Pattern match, with $options i m x s |
$expr | Use aggregation expressions in a query, so you can compare two fields |
$mod: [4, 0] | Value modulo 4 equals 0 |
$text: { $search: "..." } | Full text, requires a text index |
$jsonSchema | Match a JSON Schema, the same operator schema validation uses |
$sampleRate: 0.05 | Random ~5% of documents (4.4+) |
$where | Server-side JavaScript. Do not use it |
db.users.find({ email: { $regex: "^admin@", $options: "i" } }) db.users.find({ email: /^admin@/i }) // identical, native regex // Compare two fields in the same document db.budgets.find({ $expr: { $gt: ["$spent", "$limit"] } }) // Field present but explicitly null db.users.find({ deleted_at: { $type: "null" } })
An anchored, case-sensitive prefix regex (/^abc/) can use a normal index. Add i
and it cannot, unless the index was built with a case-insensitive collation. $where
evaluates JavaScript per document, ignores every index, and is blocked on Atlas.
$expr does the same job properly.
Array operators
| Operator | Matches |
|---|---|
$all: [a, b] | Array contains all of these values, order and extras irrelevant |
$elemMatch: { ... } | At least one single element satisfies all the inner conditions |
$size: 3 | Array length is exactly 3. No ranges, no $gt |
db.posts.find({ tags: { $all: ["mongodb", "indexes"] } }) db.posts.find({ tags: { $size: 3 } }) // One item that is BOTH sku A-1 AND qty >= 2 db.orders.find({ items: { $elemMatch: { sku: "A-1", qty: { $gte: 2 } } } })
There is no operator for "array longer than n". Either maintain a counter field or use
{ "tags.9": { $exists: true } } for "at least 10".
An array matches if any element matches
Querying an array field with a scalar tests every element. This is the single most useful and most surprising rule in MQL.
// tags: ["sale", "new", "clearance"] db.posts.find({ tags: "sale" }) // matches, "sale" is one element db.posts.find({ tags: "new" }) // matches too
| Query | Meaning |
|---|---|
{ tags: "sale" } | Any element equals "sale". Also matches a scalar tags: "sale" |
{ tags: ["sale", "new"] } | The array equals exactly ["sale","new"], in that order |
{ tags: { $all: ["sale", "new"] } } | Contains both, any order, extras allowed |
The trap is multiple conditions on one array field. Without $elemMatch they are
satisfied by different elements.
// scores: [ 5, 95 ] db.tests.find({ scores: { $gt: 90, $lt: 10 } }) // MATCHES // 95 satisfies $gt: 90, and 5 satisfies $lt: 10, separately db.tests.find({ scores: { $elemMatch: { $gt: 90, $lt: 10 } } }) // no match
The same applies to arrays of subdocuments. { "items.sku": "A-1", "items.qty": 99 }
matches an order where one item has that sku and a different item has that qty. Use
$elemMatch whenever the conditions must land on one element.
Exact subdocument vs dot notation
These two look equivalent and are not. An embedded-document equality match is a whole-document comparison, so it is field-order sensitive and rejects extra fields.
// Stored: { _id: 1, address: { city: "Lisbon", zip: "1200" } } db.users.find({ address: { city: "Lisbon", zip: "1200" } }) // matches db.users.find({ address: { zip: "1200", city: "Lisbon" } }) // NO match, wrong order db.users.find({ address: { city: "Lisbon" } }) // NO match, zip extra db.users.find({ "address.city": "Lisbon" }) // matches, always
| Form | Semantics |
|---|---|
{ a: { b: 1 } } | a equals that exact document. Same fields, same order, nothing more |
{ "a.b": 1 } | a.b equals 1, whatever else a holds and in whatever order |
Use dot notation for essentially every real query. Exact subdocument matching is only right for a fixed-shape value you always write whole, such as a compound key.
null matches missing
{ field: null } matches documents where the field is null and documents where the
field does not exist at all. There is no way to spell "is null" with a bare null.
// A: { _id: 1, deleted_at: null } // B: { _id: 2 } (no deleted_at) // C: { _id: 3, deleted_at: ISODate("2026-01-05") } db.users.find({ deleted_at: null }) // A and B db.users.find({ deleted_at: { $type: "null" } }) // A only db.users.find({ deleted_at: { $exists: false } }) // B only db.users.find({ deleted_at: { $exists: true, $ne: null } }) // C only db.users.find({ deleted_at: { $ne: null } }) // C only
| Intent | Query |
|---|---|
| Null or absent | { f: null } |
| Explicitly null | { f: { $type: "null" } } |
| Absent | { f: { $exists: false } } |
| Present and not null | { f: { $exists: true, $ne: null } } |
The same asymmetry hits $ne and $nin generally. Both match documents that lack the
field, because an absent field trivially fails to equal your value. If your soft-delete
filter is { deleted_at: null } you are relying on this, and it works, but say what you
mean in code that others will read.