Skip to main content

The MongoDB Cheat Sheet I Actually Type: Shell Queries, Updates, and Aggregation

A practical MongoDB cheat sheet for the mongosh queries you run daily — find with operators, $set updates, aggregation pipelines, and indexes, with real examples.

Published By Li Lei
#mongodb #cheat-sheet #database #backend #aggregation

The MongoDB Cheat Sheet I Actually Type

If you came from SQL, MongoDB feels familiar for about ten minutes and then stops. There are no rows and columns; there are documents and collections. A query is not a string you assemble, it is a JSON-shaped object you hand to a method. Once that clicks, mongosh becomes fast to work in. This guide is the shell reference I keep open: the find operators, the $set updates that save you from wiping a record, the aggregation pipeline, and the indexes that decide whether your endpoint takes 40 milliseconds or 3 seconds.

Everything below is paste-ready into the mongosh shell. When you want the full searchable version with 80+ entries and a production pitfall on every command, the MongoDB cheat sheet on Toolora runs it all client-side in your browser.

Documents vs Rows: The Mental Shift

In a relational table, a row is a fixed set of typed columns. In MongoDB, a document is a JSON object, and two documents in the same collection do not have to share the same shape. One user can have a phone field and the next can omit it entirely. That flexibility is the whole point, and it is also where the surprises live.

The practical consequences:

  • A "table" is a collection, and db.users is how you reference it.
  • A "row" is a document, identified by _id, which MongoDB fills with an ObjectId automatically if you do not supply one.
  • A "join" is $lookup inside an aggregation pipeline, not a first-class part of every query.
  • A missing field is not the same as a null field. This single fact causes more confused queries than anything else, because {age: {$ne: 30}} also matches documents that have no age field at all.

If you are also juggling Postgres, the side-by-side SQL cheat sheet is worth keeping nearby — the two mental models reinforce each other.

Reading Data: find With Operators

find takes a filter document. An empty filter returns everything; a filter with fields restricts the match. Operators start with $ and go inside the field's value object.

// Everyone older than 18
db.users.find({ age: { $gt: 18 } })

// Active users in one of three plans
db.users.find({ status: "active", plan: { $in: ["pro", "team", "enterprise"] } })

// Sort newest first, page of 20
db.orders.find({ paid: true }).sort({ createdAt: -1 }).limit(20)

// Only the fields you need (projection): name and email, drop _id
db.users.find({ status: "active" }, { name: 1, email: 1, _id: 0 })

The operators you reach for constantly: $gt / $gte / $lt / $lte for ranges, $in / $nin for sets, $ne for "not equal," and $exists for presence. Watch the $in array size — a 10,000-element $in is slow and ugly; reshape the query if you find yourself building one. And remember the missing-field trap: when "not equal" should also mean "the field is there," write { age: { $exists: true, $ne: 30 } }.

Writing Data: insert and the $set Rule

Inserts are simple. Updates have one rule that, if you forget it, deletes data with no error message.

// Insert one document; _id is auto-generated
db.users.insertOne({ name: "Alice", age: 31, status: "active" })

// Insert many; ordered:false keeps going after a duplicate-key error
db.users.insertMany([{ name: "Bob" }, { name: "Carol" }], { ordered: false })

// THE RULE: update with operators, never a bare object
db.users.updateOne({ name: "Alice" }, { $set: { age: 32 } })

// Increment a counter atomically
db.posts.updateOne({ _id: postId }, { $inc: { views: 1 } })

// Append to an array
db.users.updateOne({ name: "Alice" }, { $push: { tags: "vip" } })

Here is the bug that has burned every MongoDB team at least once. If you write db.users.updateOne({ name: "Alice" }, { age: 32 }) — no $set — MongoDB does not update the age field. It treats the second argument as a complete replacement document, throws away the existing record, and writes { _id: ..., age: 32 }. Email, roles, timestamps: gone. No warning, no error, the operation reports success. The discipline is permanent: a bare document means replace, a document with operators means partial update. Always wrap your changes in $set.

A Worked Example: Grouping and Counting With Aggregation

When find is not enough — totals, grouping, joins, reshaping — you reach for the aggregation pipeline. It is an array of stages, and each document flows through them in order. The two stages you will use most are $match (filter) and $group (collapse and accumulate).

The scenario: you run a shop and you want revenue and order count per status, for paid orders this year, sorted by revenue. Here is the full pipeline.

db.orders.aggregate([
  // 1. Filter first — push $match to the front so later stages do less work
  { $match: {
      paid: true,
      createdAt: { $gte: ISODate("2026-01-01") }
  }},
  // 2. Group by status, accumulate a sum and a count
  { $group: {
      _id: "$status",
      revenue: { $sum: "$total" },
      orders:  { $sum: 1 },
      avgOrder: { $avg: "$total" }
  }},
  // 3. Sort the grouped results by revenue, biggest first
  { $sort: { revenue: -1 } }
])

Read it top to bottom. $match keeps only paid orders from this year — and because it runs first, it can use an index on createdAt and shrinks the document stream before any heavy work. $group sets _id to the field you are grouping by (note the $status — the $ prefix means "the value of this field"), then $sum: "$total" adds up revenue while $sum: 1 counts documents by adding one per row. $avg gives you the mean order size for free. The output is one document per status:

{ _id: "shipped",   revenue: 184200, orders: 612, avgOrder: 301.0 }
{ _id: "pending",   revenue:  41050, orders: 188, avgOrder: 218.4 }
{ _id: "cancelled", revenue:   9300, orders:  44, avgOrder: 211.3 }

That is the entire shape of analytics in MongoDB: filter early, group with accumulators, sort the result. From there you add $project to reshape fields, $lookup to join another collection (always index the foreign field, or it is an O(N×M) scan), $unwind to flatten arrays, and $limit near the end. If a group spills past 100 MB of memory, pass { allowDiskUse: true } as the second argument to aggregate.

Indexes: Why Your Query Is Slow

The first time a MongoDB query goes from fast in staging to slow in production, the cause is almost always a missing index forcing a full collection scan.

// Single-field index
db.users.createIndex({ email: 1 })

// Compound index — field order matters; this serves
// queries on status, or status + createdAt, but NOT createdAt alone
db.orders.createIndex({ status: 1, createdAt: -1 })

// Unique only where it should be (active users), via a partial filter
db.users.createIndex(
  { email: 1 },
  { unique: true, partialFilterExpression: { deleted_at: null } }
)

// Diagnose: did the query use an index?
db.orders.find({ status: "shipped" }).explain("executionStats")

That last line is the one to memorize. Run .explain("executionStats") and look at winningPlan.stage: IXSCAN means an index served the query — good. COLLSCAN means a full scan — bad on anything past a few thousand documents. Then compare totalDocsExamined to nReturned; if you examined 1.2 million documents to return 20, you scanned the whole collection to find a handful, and you need an index. Compound indexes follow the prefix rule: an index on {status, createdAt} can answer queries on status and on status + createdAt, but it cannot serve a sort on createdAt by itself.

A First-Person Note on Building the Habit

I spent my first month with MongoDB treating it like a JSON dump and paid for it twice. Once was the $set replace bug — a "quick fix" updateOne that quietly stripped the roles field off a few hundred accounts, which I only noticed when permissions started failing the next morning. The other was an endpoint that timed out under real traffic because I had never run explain and had no idea every request was a COLLSCAN over a growing collection. Both fixes took one line. The lesson that stuck: MongoDB is forgiving about structure and unforgiving about discipline. Keep $set reflexive, run explain before you ship a query, and reach for the aggregation pipeline instead of pulling everything into application code and looping. After that, the database mostly stays out of your way.

Keep the Reference Handy

This is the working core — documents over rows, find with operators, $set updates, the $match$group$sort pipeline, and indexes you can verify with explain. The rest (replication, sharding, TTL indexes, transactions) builds on exactly these primitives. For the full searchable list with every operator and its production pitfall, open the MongoDB cheat sheet, and if your stack also runs a cache layer, the Redis cheat sheet covers the other half of most backend data flows.


Made by Toolora · Updated 2026-06-13