beginner~4h

MongoDB — CRUD, Queries & Aggregation Framework

MongoDB is the world's most popular document database. It stores data as BSON (Binary JSON) documents in collections (analogous to tables). Each document can have a different structure — true schema f

Learning objectives

  • Explain what a document database stores differently than a relational table, and why that changes how you model relationships.
  • Write a basic CRUD operation and a filtered query using MongoDB's query operators.
  • Trace how an aggregation pipeline's stages transform data step by step, similarly to how a SQL query's clauses execute in sequence.

📖 Story

Imagine, instead of a filing cabinet with separate drawers for customer info, order info, and shipping info — all cross-referenced by ID, the way a relational database would organize it — a single folder per customer that contains everything about that one customer, self-contained: their name, their addresses, their recent orders, all nested together in one document. That's the core idea of a document database like MongoDB: instead of normalizing related facts into separate tables joined by foreign keys, you often embed related data directly inside one flexible, JSON-like document (BSON, a binary superset of JSON, is MongoDB's actual on-disk format).

CRUD, MongoDB's way

insertOne/insertMany create documents. find reads them, using a query object like { status: "active", age: { $gt: 25 } } — MongoDB's query operators ($gt, $lt, $in, $and, $or, and many more) play the same role SQL's WHERE clause conditions do, just expressed as JSON rather than SQL syntax. updateOne/updateMany modify documents, typically using operators like $set to change specific fields without rewriting the whole document. deleteOne/deleteMany remove them.

The Aggregation Framework — a pipeline, not a single clause

Where SQL expresses "filter, then group, then sort" as separate clauses in one statement, MongoDB's aggregation pipeline expresses the same idea as an explicit array of stages, each one transforming the output of the stage before it: [{ $match: {...} }, { $group: {...} }, { $sort: {...} }]. This isn't a fundamentally different idea from SQL's WHEREGROUP BYHAVINGORDER BY execution order — it's the same logical pipeline, just made explicit and visible in the query's own syntax instead of being an internal execution detail.

BSON, not plain JSON

MongoDB stores documents as BSON (Binary JSON) — a binary-encoded superset of JSON that adds types plain JSON doesn't have natively (like a proper date type, and distinct integer/floating-point/decimal number types), and that's designed to be fast to traverse and index directly, rather than needing to be re-parsed from text on every access, the same reasoning behind PostgreSQL's JSONB over plain JSON.

How the aggregation pipeline actually executes

Each stage in the pipeline array runs against the output of the previous stage, in order — $match (equivalent to WHERE) filters documents first; putting it as early as possible in the pipeline is the single biggest optimization available, since it shrinks the working set every subsequent stage has to process. $group (equivalent to GROUP BY) then buckets and aggregates. $sort reorders. $project reshapes each document's fields, similar to choosing specific columns in a SQL SELECT. MongoDB's query planner can, in some cases, use an index to satisfy an early $match stage exactly the way a relational planner uses an index for a WHERE clause — the underlying reasoning (filter early, using an index, before doing expensive work) is identical across both database styles.

  • An e-commerce product catalog — each product document embeds its own variants (sizes, colors) directly, since variants are always fetched together with the product and rarely queried independently — a natural embedding case.
  • A blog platform's comments — often modeled as a separate collection (referenced by post_id), rather than embedded inside the post document, specifically because a post with thousands of comments would make the document itself unreasonably large to load just to show the post's title and body.
  • A dashboard showing "total revenue per region, this month" — a direct aggregation pipeline analog of SQL's GROUP BY/SUM: $match on the date range, $group by region summing the amount, $sort by total descending.
  • A user profile with embedded recent activity — a classic "read together, write together" embedding decision, since a user's profile page almost always needs both the profile fields and recent activity in the same request.
  • An analytics pipeline computing a rolling daily active user count — commonly built as a multi-stage aggregation pipeline ($match recent events, $group by user and day, $group again to count distinct users per day), mirroring how a SQL analyst would chain CTEs to build up the same computation step by step.
  • Embed data that's almost always read and written together (a product and its variants); reference data via a separate collection when it grows unboundedly large or is queried independently (a post's comments).
  • Put $match as early as possible in every aggregation pipeline — filtering first, before grouping or reshaping, is the single most effective performance habit in the aggregation framework.
  • Index fields used in frequent find filters and in early $match stages, exactly as you would index a WHERE-filtered column in a relational database.
  • Avoid unbounded array growth inside a single document (an order's line items are fine; a decade of a user's login history embedded in their profile document is not) — MongoDB documents have a hard 16MB size limit, and even well below that, an ever-growing embedded array degrades performance.
  • Design your schema around your application's actual read and write patterns first — document modeling is fundamentally a "how is this data accessed" decision, not a "what does the data look like on paper" decision.

⚠️ Why this keeps happening

Coming from a relational background, it's natural to reflexively normalize everything into separate collections joined by references — but that reintroduces exactly the join costs MongoDB's document model was meant to avoid; the opposite mistake (embedding everything) just as often leads to unbounded, slow-to-load documents.

  • Embedding a naturally unbounded, independently-growing list inside a document (comments, order history) — the document keeps growing indefinitely, eventually becoming slow to load and, in extreme cases, hitting MongoDB's 16MB per-document limit.
  • Over-normalizing into many small referenced collections out of relational habit, forcing multiple round trips (or a $lookup join stage) for data that's always read together anyway — giving up the exact convenience the document model was meant to offer.
  • Forgetting that $match placement in a pipeline matters. A $match placed after an expensive $group or $sort stage processes far more data than one placed first, even though the final result is identical.
  • Not indexing fields used in frequent queries, leading to full collection scans — the exact same class of mistake as an unindexed WHERE clause in SQL, just less obvious to spot in MongoDB's query syntax.
  • Assuming schema-less means schema-free, with no planning needed. MongoDB doesn't enforce a schema by default, but a genuinely inconsistent document shape across a collection (some documents have a field, others don't, or the same field holds different types) causes real application bugs, just later than a relational database's constraints would have caught them.
  • Index every field used in a frequent find filter or an early $match stage — this is as foundational to MongoDB performance as it is to relational performance.
  • Place $match and $sort stages as early as possible in a pipeline, and use $project early to drop fields you won't need in later stages, reducing the data volume flowing through the rest of the pipeline.
  • Use explain() on a query or aggregation to check whether it's using an index (IXSCAN) or scanning the full collection (COLLSCAN) — the same diagnostic instinct as EXPLAIN in SQL.
  • Favor embedding for data that's read together, specifically to avoid a $lookup (MongoDB's join-equivalent) on a hot read path — $lookup is a genuinely more expensive operation than an equivalent SQL join in many cases.
  • Set appropriate array size or entry-count limits on embedded lists that could otherwise grow without bound, to avoid the size and performance problems of an ever-growing document.
  • Establish and document your embed-vs-reference decisions per collection relationship, the same way you'd document a denormalization decision in a relational schema.
  • Monitor for slow queries and full collection scans (db.currentOp(), the MongoDB profiler) the same way you'd monitor slow-query logs in PostgreSQL.
  • Add schema validation rules (MongoDB supports optional JSON-schema-based validation per collection) for collections where data shape consistency genuinely matters, rather than relying purely on application-level discipline.
  • Review aggregation pipelines used in production dashboards for $match placement specifically whenever a pipeline is reported as slow — it's the single most common, most fixable performance issue in aggregation code.
  • Revisit an embedding decision if an embedded array that used to be small (a handful of comments) has grown, in practice, into something unbounded — this is a common schema drift that only becomes visible well after initial launch.
  1. Model the same "blog post with comments" relationship two ways — embedded and referenced — and write the find/$lookup queries each approach requires; discuss which one you'd choose and why.
  2. Write a find query using $gt, $in, and $and together, and confirm it returns the expected subset of documents.
  3. Build a three-stage aggregation pipeline ($match, $group, $sort) computing total sales per category, then reorder the stages incorrectly (sort before group) and observe the result break.
  4. Run explain() on a query filtering on an unindexed field, observe the COLLSCAN, then add an index and confirm the plan switches to IXSCAN.

✓ Quick recap

  • MongoDB stores flexible, JSON-like documents (as BSON) rather than fixed-schema rows — embedding related data together is a first-class, often preferred alternative to always referencing across separate collections.
  • MongoDB's query operators ($gt, $in, $and, etc.) fill the same role SQL's WHERE clause conditions do, just expressed as JSON.
  • The aggregation pipeline is an explicit array of stages ($match, $group, $sort, $project) — the same logical filter-then-group-then-sort idea as SQL, made visible in the syntax itself.
  • Place $match as early as possible in a pipeline — it's the single biggest aggregation performance lever, exactly like filtering early with WHERE in SQL.
  • Choose embedding for data read and written together; choose referencing for data that grows unboundedly or is queried independently — this decision is driven by access patterns, not by "what the data looks like" alone.

Want a visual for this concept?

Generate a diagram tailored to “MongoDB — CRUD, Queries & Aggregation Framework” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to MongoDB — Indexing, Replica Sets & Sharding← Back to all NoSQL chapters