πŸƒDBQuestpractice MongoDB queries
0 / 110

MongoDB cheat sheet

Every topic sheet from DBQuest on one page: 12 sections, from find() to $lookup.

How a MongoDB read is shaped

There are exactly two ways to read data. Everything else is detail.

db.coll.find(<filter>, <projection>).sort().skip().limit()

db.coll.aggregate([ <stage>, <stage>, ... ])

find is a filter plus a field picker. aggregate is a conveyor belt: each

stage rewrites the documents and hands them to the next one. If you can express

it with find, use find – it is cheaper and clearer.

The mental model that saves you

  • A query operator ($gt, $in, $elemMatch) lives inside a field:

{ seats: { $gt: 50 } }.

  • An aggregation expression ($sum, $multiply, $toUpper) lives inside a

stage and refers to fields with a $ prefix: { $multiply: ["$seats", "$price"] }.

Those two $ mean different things. "$seats" inside find() is just the

string "$seats" – a common source of silent wrong answers.

Operator index

GroupOperators
Compare$eq $ne $gt $gte $lt $lte $in $nin $mod
Logic$and $or $not $nor $expr
Element$exists $type
Text$regex $options $text
Array$all $size $elemMatch $slice
Stages$match $project $addFields $set $unset $group $sort $limit $skip $count $unwind $lookup $graphLookup $facet $bucket $sortByCount $replaceRoot $out $merge
Accumulators$sum $avg $min $max $push $addToSet $first $last $count
Expressions$multiply $divide $add $subtract $concat $toUpper $toLower $substr $size $slice $cond $ifNull $switch $year $month $dayOfMonth $dateToString

Five mistakes that cost everyone an hour

1. Two conditions on the same field written as two keys – the second silently

wins. Use one object, or $and.

2. Multiple conditions on an array without $elemMatch – different elements can

satisfy different conditions.

3. Expecting $ne / $nin to skip documents where the field is missing. They

do not; missing counts as "not equal".

4. Sorting before $group and expecting the order to survive. It does not.

5. Forgetting that $lookup always produces an array, even for one match.

◍ Finding & Projection

The shape of a read

db.<collection>.find(<filter>, <projection>)
  • filter – which documents. {} or omitted means all of them.
  • projection – which fields come back.

Projection rules

You writeYou get
{ name: 1, borough: 1 }name, borough and _id
{ name: 1, _id: 0 }name only
{ grades: 0 }everything except grades
{ "address.street": 1, _id: 0 }just the nested street

You cannot mix 1s and 0s in one projection – the only exception is turning

_id off while turning other fields on.

Other reads

db.restaurants.findOne({ borough: "Bronx" })   // one doc, not an array
db.restaurants.countDocuments({ price: 1 })     // a number
db.restaurants.distinct("cuisine")              // array of unique values

Gotchas

  • find() returns a cursor, not an array. In the shell it prints for you.
  • Dotted paths need quotes: { "address.zipcode": "10011" }.

Practise Finding & Projection β†’

β‰· Comparison Operators

The eight you actually use

OperatorMeaningExample
$eqequals (implicit){ price: 2 }
$nenot equal{ cuisine: { $ne: "Pizza" } }
$gt / $gtegreater / or equal{ seats: { $gt: 50 } }
$lt / $lteless / or equal{ seats: { $lte: 20 } }
$inmatches any in list{ borough: { $in: ["Bronx","Queens"] } }
$ninmatches none in list{ borough: { $nin: ["Bronx"] } }
$modremainder test{ seats: { $mod: [10, 0] } }

Ranges live in one object

// seats between 30 and 60, inclusive
db.restaurants.find({ seats: { $gte: 30, $lte: 60 } })

Two operators on the same field go in the same braces. Writing them as

two separate keys silently keeps only the last one.

Gotchas

  • $ne and $nin also match documents where the field is missing.
  • Comparisons are type-aware: "5" is not 5.
  • On an array field, the condition matches if any element satisfies it.

Practise Comparison Operators β†’

β‹ˆ Logical Operators

Implicit AND is free

// these two are identical
db.restaurants.find({ borough: "Manhattan", price: 4 })
db.restaurants.find({ $and: [ { borough: "Manhattan" }, { price: 4 } ] })

You only need $and when the same field appears twice with operators that

would collide as duplicate keys.

The four

{ $or:  [ { a: 1 }, { b: 2 } ] }   // at least one true
{ $and: [ { a: 1 }, { b: 2 } ] }   // all true
{ $nor: [ { a: 1 }, { b: 2 } ] }   // none true
{ price: { $not: { $gt: 2 } } }    // wraps an operator, not a value

$not is different

$not is a field-level operator – it goes inside a field, wrapping

another operator expression. It cannot take a plain value:

{ price: { $not: 2 } }          // βœ— error
{ price: { $not: { $eq: 2 } } } // βœ“

Gotchas

  • $nor and $not match documents where the field is absent.
  • Mixing: put $or inside $and when you need "X and (Y or Z)".

Practise Logical Operators β†’

βŒ— Element & Type

Does the field exist at all?

db.orders.find({ coupon: { $exists: true } })
db.orders.find({ coupon: { $exists: false } })

$exists: true matches even when the value is null. It is asking about the

key, not the value.

What type is it?

db.products.find({ price:  { $type: "number" } })
db.movies.find({   genres: { $type: "array"  } })
db.customers.find({ newsletter: { $type: "bool" } })

Common aliases: "double", "string", "object", "array", "bool",

"date", "null", "int", "long", "decimal".

Missing vs null vs false

Querymissing keynullfalse
{ f: { $exists: false } }βœ“βœ—βœ—
{ f: null }βœ“βœ“βœ—
{ f: { $ne: true } }βœ“βœ“βœ“

That first column is why $ne surprises people.

Practise Element & Type β†’

⁂ Pattern Matching

Two equivalent forms

db.restaurants.find({ name: { $regex: "^B" } })
db.restaurants.find({ name: /^B/ })

The three anchors you need

GoalPattern
starts with Bro/^Bro/
ends with Bar/Bar$/
contains Bar anywhere/Bar/
case-insensitive contains/bar/i or { $regex: "bar", $options: "i" }
starts with any of a set/^[BCD]/

Escaping

Dots, parens and $ are regex metacharacters:

{ email: /\.com$/ }     // literal dot, then "com" at the end

Gotchas

  • Only ^-anchored, case-sensitive regexes can use an index. /foo/i

scans the whole collection.

  • Regex works on strings only – it will not match a number field.

Practise Pattern Matching β†’

β‡… Sort, Limit & Skip

Chain it on the cursor

db.restaurants.find({ borough: "Manhattan" })
  .sort({ name: 1 })     // 1 = ascending, -1 = descending
  .skip(5)
  .limit(5)

Multi-key sort

.sort({ cuisine: 1, borough: -1 })

Key order matters: cuisine ascending first, then borough descending within

each cuisine.

Server-side order is fixed

No matter how you chain the methods, the server always applies:

sort β†’ skip β†’ limit

So .limit(5).sort(...) still sorts the whole result set first, then takes 5.

Pagination

// page N, 10 per page
.skip((N - 1) * 10).limit(10)

Large skip values get slow – real apps page by the last seen _id instead.

Gotchas

  • Without .sort() the order is not guaranteed, even if it looks stable.
  • Sorting on a missing field puts those documents first when ascending.

Practise Sort, Limit & Skip β†’

⧉ Array Queries

Matching against an array field

{ genres: "Drama" }              // array CONTAINS "Drama"
{ genres: ["Drama","Thriller"] } // array EQUALS exactly this, in this order
{ genres: { $all: ["Drama","Thriller"] } }  // contains both, any order
{ genres: { $size: 3 } }         // exactly 3 elements

Positional access

{ "grades.0.grade": "A" }        // first element's grade
{ "address.coord.1": { $gt: 40.75 } }  // second number in the array

$elemMatch – the important one

// βœ— one grade has A, ANOTHER has score < 6 – both conditions, different elements
{ "grades.grade": "A", "grades.score": { $lt: 6 } }

// βœ“ ONE SINGLE element has both
{ grades: { $elemMatch: { grade: "A", score: { $lt: 6 } } } }

Without $elemMatch, multiple conditions on the same array can be satisfied

by different elements. This is the #1 array bug.

"All elements satisfy X"

There is no $allMatch. You invert it:

// every grade scored above 5  ==  no grade scored 5 or less
{ grades: { $not: { $elemMatch: { score: { $lte: 5 } } } } }

Practise Array Queries β†’

⊞ Embedded Documents

Dot notation, always quoted

db.restaurants.find({ "address.zipcode": "10011" })
db.movies.find({ "imdb.rating": { $gte: 8 } })
db.orders.find({ "items.sku": "KB-100" })

That last one reaches into an array of subdocuments – it matches if any

item has that sku.

Whole-document match is exact and ordered

{ address: { building: "31", street: "W 8th St" } }

This matches only documents whose address has exactly those two fields,

in that order. Almost never what you want – use dot notation.

Projecting nested fields

db.restaurants.find({}, { name: 1, "address.street": 1, _id: 0 })
// => { name: "...", address: { street: "..." } }

The shape is preserved; you get a trimmed address object, not a flat key.

Gotchas

  • { "a.b": 1 } in a filter = "field a.b equals 1".

{ "a.b": 1 } in a projection = "include a.b". Same syntax, different job.

Practise Embedded Documents β†’

β›“ Aggregation Basics

A pipeline is an array of stages

db.restaurants.aggregate([
  { $match:   { borough: "Manhattan" } },
  { $project: { _id: 0, name: 1, cuisine: 1 } },
  { $sort:    { name: 1 } },
  { $limit:   5 }
])

Each stage takes the previous stage's documents and emits new ones.

$match is find's filter

Same syntax exactly. Put it first – it is the only stage that can use an

index, and everything after it does less work.

$project computes, not just selects

{ $project: {
    _id: 0,
    name: 1,
    capacityScore: { $multiply: ["$seats", "$price"] },
    shout: { $toUpper: "$cuisine" }
} }

Field references are strings starting with $.

$addFields keeps everything else

{ $addFields: { gradeCount: { $size: "$grades" } } }

Use $addFields (alias $set) when you want to add a field;

$project when you want to replace the whole shape.

$count

{ $count: "total" }   // => [ { total: 12 } ]

Practise Aggregation Basics β†’

Ξ£ Grouping & Accumulators

The shape

{ $group: {
    _id: "$borough",                    // what to group by
    count: { $sum: 1 },                 // accumulators
    avgSeats: { $avg: "$seats" }
} }

_id is mandatory. _id: null groups everything into one bucket.

Accumulators

AccumulatorDoes
{ $sum: 1 }counts documents
{ $sum: "$total" }adds a field up
{ $avg: "$seats" }mean
{ $min: } / { $max: }extremes
{ $push: "$name" }array of every value
{ $addToSet: "$cuisine" }array of distinct values
{ $first: } / { $last: }needs a $sort before it to mean anything

Compound grouping key

{ $group: { _id: { cuisine: "$cuisine", borough: "$borough" }, n: { $sum: 1 } } }

Sort after grouping

{ $group: { _id: "$cuisine", n: { $sum: 1 } } },
{ $sort:  { n: -1 } },
{ $limit: 5 }

Sorting before $group does not survive the group – regroup, then sort.

Gotchas

  • After $group, only the fields you declared exist. name is gone unless

you pushed or $first-ed it.

  • $avg skips non-numeric and missing values rather than treating them as 0.

Practise Grouping & Accumulators β†’

β‘‚ $unwind & Array Pipelines

One document per array element

{ $unwind: "$grades" }

A restaurant with 3 grades becomes 3 documents, each with grades as a single

object instead of an array. Everything else is copied.

The full form

{ $unwind: {
    path: "$grades",
    includeArrayIndex: "gradeIdx",       // adds 0,1,2...
    preserveNullAndEmptyArrays: true     // keep docs with [] or missing
} }

By default, documents with an empty or missing array are dropped.

That silently loses rows – reach for preserveNullAndEmptyArrays when counting.

The classic combo

db.restaurants.aggregate([
  { $unwind: "$grades" },
  { $match:  { "grades.grade": "A" } },
  { $group:  { _id: "$cuisine", aGrades: { $sum: 1 } } },
  { $sort:   { aGrades: -1 } }
])

unwind β†’ match β†’ group β†’ sort. Most "count things inside arrays" questions are

this exact shape.

$sortByCount is unwind's best friend

{ $unwind: "$genres" },
{ $sortByCount: "$genres" }   // == $group _id + $sum:1, then $sort desc

Gotchas

  • $match before $unwind filters documents; after $unwind it

filters individual elements. Both are useful – know which you meant.

  • Unwinding two arrays multiplies rows (a cartesian product). Rarely intended.

Practise $unwind & Array Pipelines β†’

✦ Joins & Advanced Stages

$lookup – the left outer join

{ $lookup: {
    from: "customers",
    localField: "customerId",
    foreignField: "_id",
    as: "customer"          // always an ARRAY, even for one match
} },
{ $unwind: "$customer" }     // flatten it back to an object

$bucket – histogram

{ $bucket: {
    groupBy: "$imdb.rating",
    boundaries: [0, 6, 7, 8, 10],
    default: "unrated",
    output: { count: { $sum: 1 }, titles: { $push: "$title" } }
} }

$facet – several pipelines, one pass

{ $facet: {
    byBorough: [ { $sortByCount: "$borough" } ],
    priciest:  [ { $sort: { price: -1 } }, { $limit: 3 } ]
} }

Returns one document with an array per key. Great for dashboards.

$replaceRoot – promote a subdocument

{ $replaceRoot: { newRoot: "$address" } }

Top N per group

The pattern worth memorising:

{ $sort:  { borough: 1, seats: -1 } },
{ $group: { _id: "$borough", top: { $push: "$$ROOT" } } },
{ $project: { top: { $slice: ["$top", 3] } } }

Sort first, group into an array, then $slice. $$ROOT is the whole document.

$graphLookup – recursive

{ $graphLookup: {
    from: "employees",
    startWith: "$reportsTo",
    connectFromField: "reportsTo",
    connectToField: "_id",
    as: "chain"
} }

Walks the chain until it runs out. Use for org charts, categories, threads.

Practise Joins & Advanced Stages β†’