Introduction to MongoDB: Installation, CRUD Operations, and Aggregation Framework

Introduction to MongoDB: Installation, CRUD Operations, and Aggregation Framework

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

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

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

Advantages of MongoDB

Limitations and Challenges

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.

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

  1. Design your schema around your application’s actual query patterns, not an abstract “correct” data model.
  2. Use schema validation even though MongoDB doesn’t require it, to catch structural bugs early.
  3. Index deliberately and monitor slow queries using explain() and MongoDB’s built-in profiler.
  4. Choose shard keys carefully if scaling horizontally — this decision is difficult to change later without significant re-architecture.
  5. Avoid unbounded array growth within documents; watch for collections likely to hit the 16MB document limit.
  6. Enable authentication, TLS, and network restrictions on every deployment, including development environments that might be inadvertently exposed.
  7. 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.

Exit mobile version