MongoDB is, by most measures, the most widely adopted NoSQL database in the world, and for good reason — it hits a sweet spot between the flexibility developers want from NoSQL and the query power they’re used to from relational systems. Whether you’re a student learning databases for the first time, a developer building your first API, or a DBA evaluating whether MongoDB fits a new project, this guide walks through everything from installation to real, working queries.
What Is MongoDB?
MongoDB is a document-oriented NoSQL database, meaning it stores data as flexible, JSON-like documents rather than as rows in rigid, predefined tables. Internally, MongoDB stores these documents in a binary format called BSON (Binary JSON), which extends standard JSON with additional data types like dates, binary data, and more precise numeric types.
MongoDB was first released in 2009 by a company called 10gen (later renamed MongoDB, Inc.), built specifically to address the scaling and flexibility limitations developers were running into with relational databases for certain types of applications — particularly those with rapidly evolving data structures, large volumes of semi-structured data, or a need for horizontal scalability across many servers.
Core Terminology
- Database: A container for collections, similar in concept to a database in the relational world.
- Collection: A group of documents, roughly analogous to a table, but without a fixed schema requirement.
- Document: A single record, stored as a BSON object — the MongoDB equivalent of a row, but capable of holding nested objects and arrays.
- Field: A key-value pair within a document, analogous to a column value in a specific row.
_id: Every document has a unique_idfield, automatically generated as anObjectIdif not explicitly provided, serving as the primary key.- Embedded Document: A document nested inside another document’s field, used for representing related data without a separate collection.
- Index: A data structure that improves query performance by allowing MongoDB to quickly locate documents without scanning the entire collection.
Installing MongoDB
MongoDB can be installed locally, run via Docker, or used as a fully managed cloud service through MongoDB Atlas. Here’s how to get started with each approach.
Option 1: MongoDB Atlas (Cloud, No Installation)
For beginners, MongoDB Atlas is often the easiest starting point — it’s a fully managed cloud database service with a generous free tier, requiring no local installation at all. You create an account, spin up a free cluster, whitelist your IP address, and get a connection string you can use immediately from any MongoDB driver or the mongosh shell.
Option 2: Local Installation on Ubuntu/Debian
# Import the public GPG key
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \
sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
# Add the MongoDB repository
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] \
https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | \
sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
# Update and install
sudo apt-get update
sudo apt-get install -y mongodb-org
# Start MongoDB
sudo systemctl start mongod
sudo systemctl enable mongod
Option 3: Local Installation on macOS (via Homebrew)
brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb-community@7.0
Option 4: Docker
docker run -d -p 27017:27017 --name mongodb mongo:7.0
Connecting via mongosh
Once installed, you connect to MongoDB using mongosh, the modern MongoDB shell:
mongosh "mongodb://localhost:27017"
Or for MongoDB Atlas:
mongosh "mongodb+srv://cluster0.abcd1.mongodb.net/" --apiVersion 1 --username myUser
CRUD Operations
CRUD — Create, Read, Update, Delete — is the foundation of working with any database, and MongoDB’s driver methods map cleanly onto these four operations.
Create
// Switch to (or create) a database
use bookstore
// Insert a single document
db.books.insertOne({
title: "The Pragmatic Programmer",
author: "Andrew Hunt",
year: 1999,
genres: ["software engineering", "career"],
price: 34.99,
in_stock: true
})
// Insert multiple documents at once
db.books.insertMany([
{ title: "Clean Code", author: "Robert C. Martin", year: 2008, price: 32.50 },
{ title: "Designing Data-Intensive Applications", author: "Martin Kleppmann", year: 2017, price: 44.99 }
])
Notice there’s no need to define a schema in advance — the books collection is created automatically the first time you insert into it, and each document can have a different set of fields if needed.
Read
// Find all documents
db.books.find()
// Find with a filter
db.books.find({ author: "Robert C. Martin" })
// Find with comparison operators
db.books.find({ year: { $gte: 2010 } })
// Find with multiple conditions (implicit AND)
db.books.find({ price: { $lt: 40 }, in_stock: true })
// Find with explicit OR
db.books.find({ $or: [{ year: { $lt: 2000 } }, { price: { $gt: 40 } }] })
// Find one document only
db.books.findOne({ title: "Clean Code" })
// Projection: return only specific fields
db.books.find({ author: "Robert C. Martin" }, { title: 1, price: 1, _id: 0 })
// Sorting and limiting
db.books.find().sort({ year: -1 }).limit(5)
// Querying array fields
db.books.find({ genres: "career" })
Update
// Update a single document
db.books.updateOne(
{ title: "Clean Code" },
{ $set: { price: 29.99 } }
)
// Update multiple documents
db.books.updateMany(
{ year: { $lt: 2010 } },
{ $set: { category: "classic" } }
)
// Increment a numeric field
db.books.updateOne(
{ title: "Clean Code" },
{ $inc: { view_count: 1 } }
)
// Add an item to an array field
db.books.updateOne(
{ title: "Clean Code" },
{ $push: { genres: "best-seller" } }
)
// Upsert: update if exists, insert if not
db.books.updateOne(
{ title: "Refactoring" },
{ $set: { author: "Martin Fowler", year: 1999 } },
{ upsert: true }
)
Delete
// Delete a single document
db.books.deleteOne({ title: "Clean Code" })
// Delete multiple documents matching a filter
db.books.deleteMany({ year: { $lt: 1995 } })
// Delete all documents in a collection (keeps the collection itself)
db.books.deleteMany({})
The Aggregation Framework
While basic find() queries cover simple lookups and filters, real-world applications frequently need to transform, group, and analyze data — the kind of work GROUP BY, HAVING, and window functions handle in SQL. MongoDB’s Aggregation Framework covers this ground through a pipeline of stages, where each stage takes the output of the previous stage as its input.
Basic Pipeline Structure
db.books.aggregate([
{ $match: { in_stock: true } },
{ $group: {
_id: "$author",
totalBooks: { $sum: 1 },
averagePrice: { $avg: "$price" }
}},
{ $sort: { totalBooks: -1 } }
])
This pipeline filters to only in-stock books ($match), groups the results by author to compute a count and average price per author ($group), then sorts authors by book count descending ($sort).
Common Aggregation Stages
$match: Filters documents, similar tofind()‘s query filter — best used early in the pipeline to reduce the working data set.$group: Groups documents by a specified key and computes aggregate values ($sum,$avg,$min,$max,$push,$addToSet).$project: Reshapes documents — selecting, renaming, or computing new fields.$sort: Orders documents by one or more fields.$limit/$skip: Controls pagination of results.$unwind: Deconstructs an array field, producing one output document per array element — essential when you need to treat array elements as individual rows for grouping or filtering.$lookup: Performs a left outer join against another collection.$facet: Runs multiple aggregation pipelines in parallel on the same input, useful for generating several different summaries (like a total count alongside a paginated result set) in one round trip.
A More Complex Example
db.orders.aggregate([
{ $match: { status: "completed", order_date: { $gte: ISODate("2026-01-01") } } },
{ $unwind: "$items" },
{ $group: {
_id: "$items.product_id",
totalRevenue: { $sum: { $multiply: ["$items.price", "$items.qty"] } },
totalUnitsSold: { $sum: "$items.qty" }
}},
{ $sort: { totalRevenue: -1 } },
{ $limit: 10 },
{ $lookup: {
from: "products",
localField: "_id",
foreignField: "product_id",
as: "product_details"
}},
{ $unwind: "$product_details" },
{ $project: {
_id: 0,
product_name: "$product_details.name",
totalRevenue: 1,
totalUnitsSold: 1
}}
])
This pipeline calculates the top 10 best-selling products by revenue for completed orders since the start of the year, joins in product details to get readable names, and shapes the final output to include only the fields we care about. This is a genuinely complex analytical query, expressed entirely within MongoDB’s native query capabilities — no separate data warehouse or ETL step required for many use cases.
Data Modeling Basics in MongoDB
MongoDB’s flexible schema is powerful, but that flexibility puts more design responsibility on the developer. A few foundational principles:
Schema flexibility doesn’t mean no schema. Even though MongoDB doesn’t enforce a rigid structure, your application still expects a consistent shape for most documents in a collection. MongoDB supports optional schema validation rules (via $jsonSchema) that you can apply to a collection to catch malformed documents at write time, which is worth using in most production applications.
Embed for one-to-few relationships, reference for one-to-many at scale. A blog post can embed its 3 most recent comments directly, but should reference a separate comments collection for its full comment history.
Design collections around access patterns. If your application always fetches a user’s profile along with their most recent 10 orders, consider whether embedding a summary of recent orders directly in the user document (updated as new orders come in) would save a query, versus always querying the orders collection separately.
Indexing Basics
Even a brief MongoDB introduction should touch on indexing, since query performance without proper indexes degrades quickly as collections grow.
// Create a single-field index
db.books.createIndex({ author: 1 })
// Create a compound index
db.books.createIndex({ author: 1, year: -1 })
// Check which indexes exist
db.books.getIndexes()
// See whether a query is using an index
db.books.find({ author: "Robert C. Martin" }).explain("executionStats")
By default, every collection has an index on _id, but any other field used frequently in query filters, sorts, or joins ($lookup) should generally be indexed too.
Multi-Document Transactions
For a long time, one of the most common criticisms of MongoDB was its lack of multi-document transactional guarantees — every write to a single document was always atomic, but there was no way to atomically update multiple documents (potentially across multiple collections) together. Since MongoDB 4.0, this gap has been closed with full ACID-compliant multi-document transactions.
const session = client.startSession();
try {
session.startTransaction();
const orders = db.collection("orders");
const inventory = db.collection("inventory");
await orders.insertOne(
{ customer_id: "cust123", item: "Widget", qty: 2, status: "pending" },
{ session }
);
await inventory.updateOne(
{ item: "Widget" },
{ $inc: { quantity: -2 } },
{ session }
);
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
This pattern is useful for cases like an order-and-inventory update that must both succeed or both fail together — but it’s worth using judiciously. Multi-document transactions carry more performance overhead than single-document atomic operations, and much of MongoDB’s design philosophy still favors modeling data (via embedding and the aggregate pattern) so that related data lives together in a single document, avoiding the need for cross-document transactions in the first place wherever reasonably possible.
Change Streams
Change Streams let applications subscribe to real-time notifications of data changes in a collection, database, or entire deployment, without needing to poll for updates or build custom mechanisms on top of the oplog directly.
const changeStream = db.collection("orders").watch();
changeStream.on("change", (change) => {
console.log("Change detected:", change);
// e.g., trigger a notification, update a cache, sync to another system
});
// Watch for specific types of changes only
const pipeline = [{ $match: { operationType: "insert" } }];
const insertsOnly = db.collection("orders").watch(pipeline);
Change Streams are commonly used to power real-time dashboards, keep search indexes or caches in sync with the primary database, trigger downstream microservice events, or implement audit logging — all without the overhead and fragility of a custom polling mechanism.
Real-World Use Cases
- Content management systems: Flexible schemas suit varied content types (articles, videos, product pages) without needing a rigid, one-size-fits-all table structure.
- Product catalogs: E-commerce products often have wildly different attributes depending on category (a shirt has a size and color; a laptop has RAM and storage), which MongoDB’s schema flexibility handles naturally.
- Real-time analytics dashboards: The Aggregation Framework’s pipeline model is well suited to computing rolling metrics and summaries directly from operational data.
- Mobile and IoT applications: Frequently evolving data structures from client apps or connected devices map well onto MongoDB’s flexible document model.
- Content personalization and user profiles: Storing user preferences, behavior history, and settings as nested documents within a single user record.
Advantages of MongoDB
- Flexible schema that adapts easily as application requirements evolve.
- Rich query language, including a genuinely powerful Aggregation Framework rivaling much of SQL’s analytical capability.
- Horizontal scalability via built-in sharding for very large datasets.
- Strong developer ergonomics — documents map naturally onto objects in most programming languages, reducing the “object-relational impedance mismatch” common with SQL databases.
- Wide ecosystem support, with official drivers for virtually every major programming language, plus tools like MongoDB Compass for visual data exploration.
Limitations and Challenges
- No native joins at scale —
$lookupworks but doesn’t perform as well as relational joins for very large datasets, pushing developers toward denormalization instead. - Document size limit — a single document is capped at 16MB, which occasionally forces awkward workarounds for use cases with large embedded data.
- Schema flexibility can become a liability without discipline — inconsistent document shapes across a collection can create real maintenance headaches if not managed carefully.
- Transactions across multiple documents, while supported since MongoDB 4.0, come with more overhead than single-document atomic operations and should be used judiciously.
Security Considerations
MongoDB requires deliberate configuration to be secure, especially for self-hosted deployments — historically, many publicly exposed and unsecured MongoDB instances have been targeted by attackers, which underscores why security configuration should never be skipped.
- Enable authentication (
--authflag orsecurity.authorization: enabledin the config file) — MongoDB does not require authentication by default in some local installation paths. - Use role-based access control (RBAC) to grant users only the permissions they actually need (read-only, read-write, admin) scoped to specific databases.
- Enable TLS/SSL for all client-server communication in production.
- Enable encryption at rest, available in MongoDB Enterprise and MongoDB Atlas by default.
- Restrict network access — never bind MongoDB to a public IP without a firewall or VPC restricting access; MongoDB Atlas enforces IP allowlisting by default.
- Audit logging, available in Enterprise/Atlas tiers, tracks access and changes for compliance and forensic purposes.
Scalability Considerations
MongoDB scales horizontally through sharding — splitting a collection’s data across multiple servers (shards) based on a shard key. Choosing a good shard key is critical: it should distribute both data and query load evenly across shards, avoiding “hot shards” that receive disproportionate traffic.
For high availability rather than pure scale, MongoDB uses replica sets — a primary node handling writes, with secondary nodes replicating data and available to take over automatically if the primary fails, plus optionally serving read traffic for read-heavy workloads.
Best Practices
- Design your schema around your application’s actual query patterns, not an abstract “correct” data model.
- Use schema validation even though MongoDB doesn’t require it, to catch structural bugs early.
- Index deliberately and monitor slow queries using
explain()and MongoDB’s built-in profiler. - Choose shard keys carefully if scaling horizontally — this decision is difficult to change later without significant re-architecture.
- Avoid unbounded array growth within documents; watch for collections likely to hit the 16MB document limit.
- Enable authentication, TLS, and network restrictions on every deployment, including development environments that might be inadvertently exposed.
- Use connection pooling in your application drivers to avoid overwhelming the database with excessive concurrent connections.
Conclusion
MongoDB earns its popularity by striking a genuinely useful balance: the schema flexibility and horizontal scalability developers want from NoSQL, combined with query and aggregation capabilities powerful enough to handle real analytical workloads without immediately reaching for a separate system. Getting comfortable with its CRUD operations is straightforward — most developers pick that up within a day — but the real skill worth investing in is the Aggregation Framework and thoughtful schema design, since those are what separate a MongoDB deployment that scales gracefully from one that turns into a maintenance headache as the application grows.