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 WHERE → GROUP BY → HAVING → ORDER 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:$matchon the date range,$groupby region summing the amount,$sortby 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 (
$matchrecent events,$groupby user and day,$groupagain 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
$matchas 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
findfilters and in early$matchstages, exactly as you would index aWHERE-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
$lookupjoin stage) for data that's always read together anyway — giving up the exact convenience the document model was meant to offer. - Forgetting that
$matchplacement in a pipeline matters. A$matchplaced after an expensive$groupor$sortstage 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
WHEREclause 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
findfilter or an early$matchstage — this is as foundational to MongoDB performance as it is to relational performance. - Place
$matchand$sortstages as early as possible in a pipeline, and use$projectearly 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 asEXPLAINin SQL. - Favor embedding for data that's read together, specifically to avoid a
$lookup(MongoDB's join-equivalent) on a hot read path —$lookupis 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
$matchplacement 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.
- Model the same "blog post with comments" relationship two ways — embedded and referenced — and write the
find/$lookupqueries each approach requires; discuss which one you'd choose and why. - Write a
findquery using$gt,$in, and$andtogether, and confirm it returns the expected subset of documents. - 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. - Run
explain()on a query filtering on an unindexed field, observe theCOLLSCAN, then add an index and confirm the plan switches toIXSCAN.
✓ 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'sWHEREclause 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
$matchas early as possible in a pipeline — it's the single biggest aggregation performance lever, exactly like filtering early withWHEREin 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 →