R
Rishtaara
Knowledge Hub
Technology & IT

MongoDB: Zero to Production — Notes & Detailed Queries

By Rishtaara Editorial Team55 min read
#MongoDB#NoSQL#Database#Aggregation#Mongoose

Complete MongoDB tutorial — NoSQL basics, CRUD, query/update operators, aggregation, indexing, replication, sharding, security, Mongoose, and Node.js projects with copy-paste examples.

What will I learn in this course?

This Rishtaara MongoDB Fundamentals course takes you from zero to production-ready basics. You start with NoSQL concepts, install MongoDB on your machine, and learn the document model that powers modern web and mobile apps.

By the end you will write CRUD queries, use operators and aggregation, design schemas with embed vs reference, connect Node.js with Mongoose, and understand replication, indexing, and security.

Real-life example: This course is like learning to run a warehouse — first you understand what goes on the shelves (documents), then how to receive stock (insert), find items (find), update inventory (update), and ship orders (aggregation pipelines).

  • NoSQL basics and when MongoDB beats relational databases
  • Install MongoDB on Windows, macOS, and Ubuntu — plus Docker and Atlas
  • MongoDB Compass, mongosh, and Atlas cloud setup
  • Databases, collections, documents, BSON types, and ObjectId
  • CRUD — insertOne/Many, find, updateOne/Many, deleteOne/Many
  • Query operators ($gt, $in, $regex, $elemMatch) and aggregation ($match, $group, $lookup)
  • Indexing, schema design, Node.js + Mongoose, transactions, and security
Tip: Basic JavaScript helps — you will write queries in mongosh and connect from Node.js. HTML/CSS/React knowledge is optional but useful if you build fullstack apps later.
Rishtaara MongoDB course path

What is MongoDB?

MongoDB is an open-source, document-oriented NoSQL database. Data is stored as BSON (Binary JSON) — a flexible, JSON-like format that maps naturally to objects in JavaScript, Python, and Java.

Unlike rigid SQL tables, MongoDB uses dynamic schemas. Documents in the same collection can have different fields. That makes it ideal when product requirements change often — new features add fields without painful migrations.

MongoDB runs on Linux, Windows, and macOS. It scales horizontally with sharding and stays available with replication. The default port is 27017.

Real-life example: A SQL table is a spreadsheet where every row must have the same columns. MongoDB is a folder of sticky notes — each note (document) can have different details, as long as it lives in the right collection.

  • Document database — records are JSON-like BSON documents
  • Dynamic schema — fields can differ per document in one collection
  • Open source — Community Edition is free for learning and many production workloads
  • Cross-platform — install locally or use MongoDB Atlas in the cloud
  • Scalable — sharding splits data across servers; replication copies data for uptime

Why learn MongoDB?

MongoDB is one of the most popular databases for web apps, APIs, and real-time products. Its document model matches how frontend developers think — objects and arrays instead of normalized rows spread across ten tables.

Major companies use MongoDB in production: eBay for catalog data, Uber for geospatial and operational workloads, Adobe for content platforms. Startups pick it because they can ship features fast without rewriting schemas every sprint.

Drivers exist for JavaScript/Node.js, Python, Java, Go, and more. If you know one language, the MongoDB query patterns transfer across stacks.

Real-life example: Learning MongoDB is like learning to pack a suitcase instead of folding clothes into fixed drawers. Same trip (your app), but you fit odd-shaped items (nested data) without forcing everything into identical boxes.

  • Popular in MERN/MEAN stacks — MongoDB + Express + React/Angular + Node.js
  • Schema-less flexibility — evolve your data model as the product grows
  • Built-in replication and sharding for high availability and scale
  • Rich ecosystem — Compass GUI, Atlas cloud, Mongoose ODM for Node.js
  • Strong hiring demand — backend and fullstack roles often list MongoDB

Hello World — insert your first document with Node.js

The fastest way to feel MongoDB is to connect from Node.js and insert one document. Install the official driver, point it at localhost:27017, and write to a database called testdb.

This mirrors what most Rishtaara fullstack projects do: an API route receives JSON, and the driver stores it as a BSON document.

Real-life example: insertOne is like dropping a single file into a labeled cabinet drawer. The database (testdb) is the building, users is the drawer, and the document is the file with name and email on it.

Tip: Make sure MongoDB is running locally (mongod) or swap the URI for your Atlas connection string before running this script.
Install the MongoDB Node.js driver
npm init -y
npm install mongodb
Hello World — connect and insertOne
const { MongoClient } = require("mongodb");

const uri = "mongodb://localhost:27017";
const client = new MongoClient(uri);

async function run() {
  try {
    await client.connect();
    const db = client.db("testdb");
    const users = db.collection("users");

    const result = await users.insertOne({
      name: "Asha Kumar",
      email: "asha@example.com",
      joinedAt: new Date(),
    });

    console.log("Inserted document with _id:", result.insertedId);
  } finally {
    await client.close();
  }
}

run().catch(console.error);

What is NoSQL?

NoSQL (Not Only SQL) describes databases that store and retrieve data differently from traditional relational systems. They trade rigid tables and JOIN-heavy queries for flexible models tuned to modern app patterns — high traffic, changing schemas, and horizontal scale.

Relational databases excel when data is highly structured, relationships are complex, and ACID transactions across many tables are central. NoSQL systems excel when you need speed of development, nested documents, or massive scale on commodity hardware.

Real-life example: A relational database is a library catalog with strict Dewey Decimal rules — every book must fit the system. NoSQL is a personal bookshelf where novels, comics, and notebooks sit together; you organize by how you actually read them.

  • Flexible data models — documents, key-value pairs, wide columns, or graphs
  • Horizontal scaling — add more servers instead of one bigger machine
  • Schema flexibility — evolve structure without ALTER TABLE migrations
  • Optimized for specific access patterns — not one-size-fits-all SQL

Key features vs relational databases

Relational databases normalize data into tables linked by foreign keys. Queries JOIN tables to rebuild the view your app needs. NoSQL often embeds related data in one document so reads are a single fetch.

SQL enforces schema at write time — every row must match column definitions. MongoDB validates optionally; by default documents can differ within a collection.

Real-life example: SQL is splitting a pizza order across three clipboards — customer sheet, topping sheet, delivery sheet — then stapling them for every read. NoSQL puts the whole order on one ticket.

  • SQL: tables, rows, columns, fixed schema, JOINs for relationships
  • NoSQL: collections/documents or other models, flexible fields, embed or link
  • SQL: vertical scale (bigger CPU/RAM) is the classic path
  • NoSQL: horizontal scale (more nodes) is a first-class design goal
  • Both can be ACID — MongoDB supports multi-document transactions on replica sets

Four main types of NoSQL databases

Document stores (MongoDB) keep records as BSON/JSON documents in collections. Key-value stores (Redis) map keys to simple values for caching and sessions. Column-family stores (Cassandra) optimize wide rows for time-series and IoT. Graph databases (Neo4j) model nodes and edges for social networks and recommendations.

MongoDB is the document type — the best fit when your app works with objects, nested arrays, and semi-structured content.

Real-life example: Picking a NoSQL type is like picking luggage — a duffel (document) for mixed clothes, a key locker (key-value) for one item per slot, a filing cabinet with wide folders (column), or a map with pins and strings (graph).

  • Document — MongoDB, flexible JSON-like records (this course focus)
  • Key-Value — Redis, fast lookups by key
  • Column — Cassandra, optimized for write-heavy, distributed rows
  • Graph — Neo4j, relationships are first-class citizens
NoSQL database types

When NoSQL fits modern apps

Choose NoSQL when your data looks like documents (user profiles, product catalogs, content feeds), when schemas change weekly, or when you need to scale reads/writes across many servers.

Stick with SQL when you need complex reporting JOINs, strict relational integrity as the default pattern, or a mature SQL toolchain your team already owns.

Many Rishtaara-style production stacks use both: PostgreSQL for billing and MongoDB for user-generated content.

Real-life example: A social media feed is a stream of varied posts — text, images, polls — that changes shape often. That is document territory. A bank ledger with double-entry rules is relational territory.

Tip: "NoSQL" does not mean "no structure." Good MongoDB apps still design schemas carefully — they just optimize for how the app reads data, not for third normal form.

Core concepts and functionalities

MongoDB organizes data in a hierarchy: a MongoDB server (mongod) hosts multiple databases. Each database contains collections. Each collection holds documents — the unit of storage and retrieval.

Every document has a unique _id field (ObjectId by default). Fields are key-value pairs; values can be strings, numbers, arrays, embedded documents, dates, and more.

MongoDB provides CRUD operations, rich query filters, aggregation pipelines for analytics, indexes for speed, and replication/sharding for production resilience.

Real-life example: Think of MongoDB as a company — the server is the building, databases are departments, collections are filing cabinets, and documents are individual employee files with different optional sections.

  • Database — logical namespace (e.g. rishtaara, ecommerce)
  • Collection — group of documents (like a table without fixed columns)
  • Document — one record, up to 16 MB, stored as BSON
  • Field — name/value pair inside a document
  • Index — data structure that speeds up queries (like a book index)

How MongoDB stores and retrieves data

When you insert a document, the storage engine writes BSON to disk and updates indexes. When you find with a filter, the query planner picks an index or scans the collection, then returns a cursor you iterate.

Documents in one collection can sit in the same data files but have different shapes. MongoDB does not require every document to declare the same fields upfront.

Real-life example: Storing data in MongoDB is like saving photos in an album app — each photo has id and date, but some have captions, some have location tags, and some have neither. The album (collection) still works.

Hierarchy in mongosh
// Server → database → collection → document
use rishtaara          // switch to database
db.users.insertOne({   // collection: users
  name: "Rahul",
  email: "rahul@example.com"
})
db.users.find()        // retrieve documents
MongoDB document model

Advantages over traditional RDBMS for flexible modeling

Relational models shine when every entity is uniform and JOINs are cheap at your scale. MongoDB shines when entities carry nested data (addresses, order line items, comment threads) and you want one read to return everything the UI needs.

Adding a new optional field in MongoDB is often as simple as writing it on new documents — no migration script blocking deploys. You can add JSON Schema validation later when the model stabilizes.

Real-life example: Modeling a shopping cart in SQL might need cart, cart_item, and product tables joined on every page load. In MongoDB, one cart document can embed an items array — open the cart, one query, done.

  • Embed related data — fewer round trips between app and database
  • Natural mapping to JSON APIs — what the client sends is what you store
  • Fast iteration — prototype features without schema migration meetings
  • Horizontal scaling path — sharding when single-server limits are hit
  • Optional validation — add rules when flexibility should tighten
Tip: Flexibility is not permission to skip design. Plan which data you embed vs reference — Rishtaara later lessons cover embed vs $lookup patterns in depth.

Side-by-side comparison

Relational databases organize data in tables with fixed columns. MongoDB organizes data in collections of documents with flexible fields. The mental model shift is from rows-and-JOINs to documents-and-embed-or-link.

Real-life example: SQL is a spreadsheet where column A must always be "Name." MongoDB is a stack of forms where each form can have different optional sections, as long as it belongs to the right pile (collection).

  • Tables vs collections — SQL tables enforce columns; collections hold varied documents
  • Rows vs documents — a row maps to columns; a document is a self-contained object
  • JOINs vs embed / $lookup — SQL JOINs at query time; MongoDB embeds or uses aggregation $lookup
  • Schema — SQL schema-first; MongoDB schema-flexible with optional validation
  • Primary key — SQL often auto-increment or UUID; MongoDB uses ObjectId on _id by default
SQL tables vs MongoDB collections

MongoDB vs MySQL specifically

MySQL is the world's most deployed open-source relational database. It is excellent for structured business data, reporting with SQL, and apps built around normalized models. MongoDB targets document workloads, rapid schema change, and scale-out architectures.

MySQL queries use SQL with SELECT, JOIN, GROUP BY. MongoDB queries use JavaScript-like objects in find() and pipeline stages in aggregate().

Real-life example: MySQL is the accountant's ledger — precise columns, every rupee in the right row. MongoDB is the product team's whiteboard — sticky notes with sketches, prices, and notes that evolve daily.

  • MySQL — mature SQL ecosystem, strong for transactions and relational reports
  • MongoDB — BSON documents, nested arrays, geospatial and text indexes built in
  • MySQL — vertical scaling and read replicas are common patterns
  • MongoDB — sharding is a core feature for very large datasets
  • Both — widely hosted (RDS, Atlas), both used in production at massive scale
Same data — MySQL vs MongoDB
-- MySQL: normalized users table
CREATE TABLE users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100),
  email VARCHAR(255) UNIQUE
);
INSERT INTO users (name, email) VALUES ('Asha', 'asha@example.com');
MongoDB equivalent
db.users.insertOne({
  name: "Asha",
  email: "asha@example.com"
  // _id: ObjectId(...) added automatically
})

When to pick each

Pick MySQL (or PostgreSQL) when relationships are complex, data integrity across many tables is non-negotiable, and your team lives in SQL for analytics.

Pick MongoDB when documents map to your domain, nested data is common, the schema evolves quickly, or you are building a Node.js/JavaScript stack that speaks JSON end to end.

Real-life example: An inventory system with strict stock accounting might live in MySQL. A blog with posts, tags, comments, and media metadata might live in MongoDB — one post document can embed comments for fast reads.

  • Choose MySQL — ERP, banking core, heavy reporting JOINs, fixed schemas
  • Choose MongoDB — content platforms, IoT events, mobile backends, catalogs
  • Hybrid — SQL for money, MongoDB for profiles and activity (very common)
  • Wrong reason to pick MongoDB — "SQL is hard" (learn both on Rishtaara)
Tip: Interviews often ask this comparison. Memorize tables vs collections, JOINs vs embed, and one honest tradeoff — flexibility vs enforced relational structure.

Install on Windows

Download MongoDB Community Server from mongodb.com/try/download/community. Run the MSI installer, choose Complete setup, and optionally install MongoDB as a Windows service so it starts on boot.

Install mongosh separately if the installer does not include it — mongosh is the modern shell for running queries. Default data directory is C:\Program Files\MongoDB\Server\<version>\data.

Real-life example: Installing on Windows is like setting up a home printer — run the installer, plug in the defaults, then print a test page (mongosh ping) to confirm it works.

  • Download Community Server MSI from official MongoDB site
  • Install mongosh — the interactive shell (replaces legacy mongo CLI)
  • Optional: MongoDB Compass GUI from mongodb.com/try/download/compass
  • Ensure port 27017 is not blocked by firewall for local dev
Verify MongoDB on Windows
# After install, open PowerShell or CMD
mongosh

# Inside mongosh
db.runCommand({ ping: 1 })
# { ok: 1 } means the server is reachable

Install on macOS with Homebrew

Homebrew is the simplest path on Mac. Tap the MongoDB brew repository, install mongodb-community, and start the service with brew services.

Real-life example: Homebrew install is like ordering a coffee kit subscription — one command delivers the beans (server) and mug (mongosh path) to your kitchen.

Tip: Apple Silicon Macs use the same commands. If brew warns about PATH, follow its post-install instructions to add mongosh to your shell profile.
macOS Homebrew install
brew tap mongodb/brew
brew install mongodb-community@7.0
brew services start mongodb-community@7.0
mongosh

Install on Ubuntu with apt

Import MongoDB's GPG key and apt repository for your Ubuntu version, then apt install mongodb-org. Enable and start mongod with systemctl.

Real-life example: Ubuntu apt install is like registering with a local grocer — add their address (repo), then weekly delivery (packages) lands in your pantry.

Ubuntu apt install (overview)
# Follow current docs at mongodb.com/docs/manual/installation/
# Typical flow:
sudo apt-get install -y mongodb-org
sudo systemctl start mongod
sudo systemctl enable mongod
mongosh

Docker quick start and verify with mongosh

Docker is the fastest way to run MongoDB without touching OS packages. One container exposes port 27017; exec into it for mongosh or connect from your host.

Real-life example: Docker MongoDB is a pop-up kiosk — open for business in seconds, tear down when class ends, no permanent renovation of your laptop.

Tip: For Rishtaara exercises, local Docker or Atlas free tier both work. Pick one and stay consistent through the course.
Run MongoDB in Docker
docker run -d --name mongo -p 27017:27017 mongo:7

# Shell inside container
docker exec -it mongo mongosh

# Or from host (if mongosh installed locally)
mongosh "mongodb://localhost:27017"
Quick sanity check
show dbs
use rishtaara
db.getName()
db.runCommand({ ping: 1 })

Installing MongoDB Compass

MongoDB Compass is the official GUI for browsing databases, running queries, and inspecting indexes. Download it from mongodb.com/try/download/compass for Windows, macOS, or Linux.

Compass connects to localhost:27017 for local servers or to Atlas connection strings for cloud clusters. You do not need Compass to use MongoDB — mongosh is enough — but visuals help beginners.

Real-life example: Compass is Google Maps for your data — mongosh is turn-by-turn directions. Both get you there; Compass shows the landscape.

  • Download Compass installer for your OS (free)
  • Launch Compass → New Connection → paste URI or use localhost:27017
  • Browse databases, collections, and documents in a tree view
  • Build filters with the query bar without memorizing every operator
Tip: On Windows, install Compass after Community Server so versions stay compatible. Connection to local MongoDB uses mongodb://localhost:27017.

Create a free MongoDB Atlas cluster

MongoDB Atlas is the managed cloud service. Sign up at mongodb.com/cloud/atlas, create a free M0 cluster, choose a cloud region close to you, and wait a few minutes for provisioning.

Create a database user (username + password) and add your IP to the access list (0.0.0.0/0 only for quick learning — tighten this for real projects).

Real-life example: Atlas is renting a storage unit with security and backup included — you bring keys (connection string), not construction tools (server admin).

  • Sign up → Build a Database → FREE shared cluster (M0)
  • Create DB user under Database Access
  • Network Access → add current IP or temporary 0.0.0.0/0 for dev
  • Clusters → Connect → Drivers → copy connection string
Atlas connection string format
mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/?retryWrites=true&w=majority

# Replace <username> and <password> with your DB user
# Append database name: ...mongodb.net/rishtaara

Connect Compass to Atlas

In Atlas, click Connect on your cluster, choose Compass, copy the connection string, paste it into Compass New Connection, replace the password placeholder, and click Connect.

You should see cluster databases (admin, local, and any you create). Create a rishtaara database by inserting your first document from Compass or mongosh.

Real-life example: Connecting Compass to Atlas is like pairing Bluetooth headphones — one pairing code (URI), then audio (data) flows wirelessly.

Tip: Never commit Atlas passwords to Git. Store MONGODB_URI in a .env file and load it in Node.js with process.env.
Test Atlas connection from mongosh
mongosh "mongodb+srv://USER:PASS@cluster0.xxxxx.mongodb.net/rishtaara"

# Then
db.students.insertOne({ name: "Demo", course: "MongoDB Basics" })
db.students.find()

Terminology — database, collection, document

A database is a container for collections — similar to a SQL database name. A collection groups related documents — like a table but without enforced identical columns. A document is one BSON record with fields and values.

MongoDB creates databases and collections lazily: the first write to db.myCollection.insertOne(...) creates them if they did not exist.

Real-life example: Database = school building. Collection = classroom. Document = one student's report card. Different students can have different optional sections on their cards.

  • Database — namespace boundary (permissions, backups often scoped here)
  • Collection — set of documents (naming: lowercase, plural is convention, e.g. users)
  • Document — BSON object with _id; max size 16 MB
  • Field — key: value inside a document (dot notation for nested: address.city)

createCollection, show dbs, and use

use rishtaara switches the shell context to the rishtaara database (creates it on first write). show dbs lists database names. show collections lists collections in the current database.

createCollection is optional — inserts create collections automatically — but explicit creation lets you set options like capped collections or validators.

Real-life example: use db is walking into a specific office wing. show dbs is the building directory. createCollection is labeling a new filing cabinet before anyone drops files in.

Tip: admin and local are system databases — avoid storing app data in admin. Your app database might be rishtaara, ecommerce, or testdb.
Navigate and create collections
show dbs

use rishtaara
db.getName()              // "rishtaara"

db.createCollection("courses")
show collections

// Implicit create on first insert
db.students.insertOne({ name: "Priya", grade: 11 })
show collections          // students appears

Sample document structure

Documents mirror JSON objects: strings in double quotes in strict JSON, but mongosh accepts unquoted keys. Arrays hold lists; embedded objects represent nested one-to-one or one-to-few data.

Every document needs _id — MongoDB generates ObjectId if you omit it.

Real-life example: A user document is a contact card — name at the top, phone list in the middle, address block at the bottom. One card, many field types.

Sample user document
{
  _id: ObjectId("665a1b2c3d4e5f6789012345"),
  name: "Asha Kumar",
  email: "asha@example.com",
  roles: ["student", "editor"],
  profile: {
    city: "Mumbai",
    bio: "Learning MongoDB on Rishtaara"
  },
  enrolledCourses: [
    { slug: "mongodb-fundamentals", progress: 12 },
    { slug: "nodejs-basics", progress: 45 }
  ],
  active: true,
  createdAt: ISODate("2024-06-01T00:00:00Z")
}
Insert and read the document
db.users.insertOne({
  name: "Asha Kumar",
  email: "asha@example.com",
  roles: ["student"],
  active: true,
  createdAt: new Date()
})

db.users.findOne({ email: "asha@example.com" })

Introduction to JSON

JSON (JavaScript Object Notation) is a text format for objects and arrays. Keys are strings; values can be strings, numbers, booleans, null, arrays, or nested objects. APIs and browsers use JSON constantly.

MongoDB documents look like JSON but are stored as BSON on disk — richer types and faster parsing than plain text JSON.

Real-life example: JSON is a handwritten note anyone can read. BSON is the same note scanned into a structured digital form — machines read it faster and store extra metadata.

  • Object — { "key": "value" }
  • Array — [1, 2, "three"]
  • JSON has no Date or binary type — BSON adds those
  • Valid JSON requires double quotes on keys and strings
Valid JSON example
{
  "name": "Rahul",
  "scores": [88, 92, 79],
  "passed": true,
  "address": {
    "city": "Delhi",
    "pincode": "110001"
  }
}

What is BSON and why binary?

BSON (Binary JSON) extends JSON with additional types: Date, ObjectId, int32/int64, Decimal128, Binary, Timestamp, and more. Field names are stored with length prefixes so parsing skips scanning.

Binary storage is compact and fast to traverse — important when millions of documents live on disk. Drivers convert between BSON and native language types (JavaScript objects, Python dicts).

Real-life example: JSON is a paperback — human-friendly. BSON is the hardcover library binding — slightly heavier to produce, but libraries (databases) shelve and retrieve it efficiently at scale.

  • BSON is a binary encoding — not meant for humans to read raw
  • Drivers handle BSON ↔ language objects automatically
  • Extra types cover real app needs — dates, decimals, unique IDs
  • Document limit 16 MB — large files use GridFS

Common BSON types in MongoDB

Know these types when writing queries and designing schemas. Wrong type assumptions (string "42" vs number 42) cause filters to miss documents.

Real-life example: BSON types are ingredient labels — flour vs sugar look similar in a jar, but using the wrong one ruins the recipe (query).

  • String — UTF-8 text (names, emails, slugs)
  • Number — int32, int64, double, Decimal128 (use Decimal128 for money when needed)
  • Boolean — true / false
  • Date — UTC datetime (ISODate in shell)
  • Array — ordered list of values
  • Object / Embedded document — nested key-value object
  • ObjectId — 12-byte identifier for _id
  • Null — explicit null value
  • Binary — raw bytes (images, files metadata)
  • Regular Expression — pattern matching in queries
Tip: In JavaScript drivers, new Date() becomes BSON Date. Plain JSON from fetch() has ISO date strings — convert before insert if you need Date type queries.
Document using multiple BSON types
db.products.insertOne({
  name: "Wireless Mouse",           // String
  price: NumberDecimal("899.00"),   // Decimal128 for exact money
  stock: 42,                        // Number (int/double)
  inStock: true,                    // Boolean
  tags: ["wireless", "peripheral"], // Array
  specs: { dpi: 1600, wireless: true }, // Embedded document
  createdAt: new Date(),            // Date
  notes: null                       // Null
})
Query by type with $type
db.products.find({ price: { $type: "decimal" } })
db.products.find({ stock: { $type: "int" } })

ObjectId structure

ObjectId is MongoDB's default _id type — a 12-byte value guaranteed unique across a sharded cluster. It embeds a 4-byte timestamp (creation time), 5-byte random value, and 3-byte counter.

You can inspect ObjectId.getTimestamp() in mongosh to see when a document was created (approximate to insertion second).

Real-life example: ObjectId is a ticket number with hidden metadata — the counter clerk (MongoDB) knows when it was issued and guarantees no duplicate ticket in the venue.

  • 12 bytes — compact compared to UUID strings in every index
  • Timestamp component — rough creation time without a separate field
  • Generated automatically if _id is omitted on insert
  • Custom _id allowed — string, UUID, or compound key if you design for it
Work with ObjectId in mongosh
const doc = db.users.insertOne({ name: "Test User" })
const id = doc.insertedId

id                        // ObjectId('...')
id.getTimestamp()         // ISODate — creation second

db.users.findOne({ _id: id })

// Create ObjectId from string (must be valid 24 hex chars)
ObjectId("665a1b2c3d4e5f6789012345")

What is a cursor?

find() does not return all documents at once — it returns a cursor, a pointer that streams results from the server in batches. This keeps memory low on large collections.

Call toArray() to load everything into an array (fine for small sets). Use forEach or async iteration in drivers for large result sets.

Real-life example: A cursor is a Netflix playlist — titles load in chunks as you scroll, not the entire catalog downloaded before you press play.

Tip: In Node.js driver, find() returns a FindCursor — use for await...of or cursor.toArray() similarly.
Cursor basics in mongosh
const cursor = db.users.find({ active: true })

// Iterate with forEach
cursor.forEach(doc => print(doc.name))

// Load all into array (small results only)
const all = db.users.find().toArray()

// Manual next()
const c = db.users.find()
printjson(c.next())
printjson(c.hasNext())

Cursor methods — limit, skip, sort, count

Chain limit(n) to cap results, skip(n) for pagination offsets, sort({ field: 1 }) for ascending order (-1 descending). countDocuments(filter) counts matches; deprecated cursor.count() is replaced by countDocuments in modern shell.

Sort + skip + limit order matters — sort first, then skip, then limit for stable pagination.

Real-life example: limit/skip/sort are search filters on a shopping site — sort by price, skip first 20 items (page 2), show 10 per page (limit).

  • limit(n) — return at most n documents
  • skip(n) — skip first n (pagination; use with indexes on sort field)
  • sort({ field: 1 | -1 }) — 1 ascending, -1 descending
  • countDocuments(filter) — accurate count; may scan if no index
limit, skip, sort
// Newest 5 active users
db.users
  .find({ active: true })
  .sort({ createdAt: -1 })
  .limit(5)

// Page 3 — 10 per page (skip 20)
db.users
  .find()
  .sort({ name: 1 })
  .skip(20)
  .limit(10)

// Count matching documents
db.users.countDocuments({ active: true })

MongoDB Atlas overview

Atlas is MongoDB's fully managed cloud database. It handles provisioning, backups, monitoring, patches, and scaling. Free M0 clusters are perfect for Rishtaara learning and small prototypes.

Atlas adds features beyond self-hosted Community Server: performance advisor, online archive, global clusters, and built-in search (Atlas Search).

Real-life example: Self-hosted MongoDB is owning a car — you maintain it. Atlas is ride-share with maintenance included — you focus on destination (your app).

  • Web UI for clusters, users, IP access, and metrics
  • Automated backups on paid tiers; snapshots configurable
  • Alerts for slow queries, disk space, and replication lag
  • Connect via mongosh, Compass, or any driver connection string

MongoDB Compass — browse and query builder

Compass visualizes databases and collections, lets you edit documents in a form view, and builds aggregation pipelines with stage helpers. Index tab shows existing indexes and suggests new ones.

Use the Documents tab filter bar for { active: true } style queries without typing mongosh. Export sample documents to JSON for fixtures.

Real-life example: Compass is a photo gallery app for your data — thumbnails (documents), albums (collections), and filters (queries) without writing shell commands.

  • Schema tab — inferred field types and frequency (sampled)
  • Indexes tab — create and drop indexes with clicks
  • Explain plan — see IXSCAN vs COLLSCAN for a query
  • Aggregation tab — build pipelines stage by stage
Tip: Practice the same query in Compass and mongosh — both skills matter in jobs where teammates share Atlas access but not terminal access.

mongosh common commands

mongosh is the daily driver for admins and developers. Master navigation, insert/find basics, and help commands — you will live here during debugging.

Real-life example: mongosh is the Swiss Army knife in your pocket — Compass is the toolbox at home. Carry the knife everywhere.

Tip: You finished MongoDB basics on Rishtaara. Next up in the course: CRUD deep dives, query operators, aggregation, indexing, and Mongoose — open lesson 11 when your local or Atlas server is running.
Essential mongosh commands
help                    // shell help
show dbs                // list databases
use rishtaara           // switch database
db                      // current db name
show collections        // list collections

db.users.insertOne({ name: "Demo" })
db.users.find()
db.users.findOne({ name: "Demo" })
db.users.updateOne({ name: "Demo" }, { $set: { active: true } })
db.users.deleteOne({ name: "Demo" })

db.users.stats()        // collection stats
db.users.getIndexes()   // list indexes
Load connection from environment (Node.js pattern)
// .env → MONGODB_URI=mongodb://localhost:27017/rishtaara
import { MongoClient } from "mongodb";

const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const db = client.db(); // database from URI path
const users = db.collection("users");

What is CRUD?

CRUD stands for Create, Read, Update, Delete — the four basic actions every database application performs. In MongoDB, these map to insert, find, update, and delete operations on documents inside collections.

Real-life example: CRUD is like managing a school register — add a new student (Create), look up a name (Read), change a grade (Update), and remove a transferred student (Delete).

Every Rishtaara backend you build — user profiles, product catalogs, order history — boils down to these four operations. Master them in mongosh first, then copy working patterns into Node.js, Python, or any driver.

  • Create — insertOne(), insertMany() add new documents.
  • Read — find(), findOne() retrieve documents with filters.
  • Update — updateOne(), updateMany(), replaceOne() modify existing documents.
  • Delete — deleteOne(), deleteMany() remove documents.
CRUD overview

Create database and collection

MongoDB creates a database the first time you write data to it. Collections are created automatically on the first insert, or you can create them explicitly with db.createCollection().

Real-life example: A database is like a filing cabinet labelled 'rishtaara'. Each drawer inside is a collection — students, products, orders — holding related documents.

MongoDB only shows a database in show dbs after it contains at least one document or collection with data. An empty database is not persisted.
Create database and collection in mongosh
// Switch to (or create) the rishtaara database
use rishtaara

// Explicitly create a collection (optional — insert also creates it)
db.createCollection("students")

// Insert creates the collection if it does not exist
db.products.insertOne({
  name: "Wireless Mouse",
  price: 899,
  category: "electronics",
  stock: 120
})

// Verify
show dbs          // rishtaara appears after first write
show collections  // students, products
Create database using MongoDB Compass
// In Compass GUI:
// 1. Click "+" next to "Databases" → enter "rishtaara" → Create Database
// 2. Enter collection name "students" → Create Collection
// 3. Click "ADD DATA" → "Insert Document" → paste JSON:

{
  "name": "Asha Khan",
  "grade": 10,
  "subjects": ["Mathematics", "Computer Science"],
  "active": true
}

// Compass runs the same insertOne under the hood.

Drop database and drop collection

db.dropDatabase() permanently removes the current database and all its collections. db.collection.drop() removes a single collection and its indexes.

Real-life example: Dropping a collection is like shredding one drawer of files. Dropping a database is like demolishing the entire filing cabinet — every drawer goes.

  • drop() removes the collection and all indexes on it.
  • dropDatabase() removes every collection in the current db.
  • On Atlas, use point-in-time restore if you drop something by mistake.
  • In development, dropping and re-seeding is common — never in production without backups.
Drop collection and database in mongosh
use rishtaara

// Drop a single collection (and its indexes)
db.temp_logs.drop()
// Returns true if collection existed, false otherwise

// Drop the entire current database
db.dropDatabase()
// Returns: { ok: 1, dropped: "rishtaara" }

// Switch back — use recreates on next write
use rishtaara
db.students.insertOne({ name: "Test", grade: 1 })
Drop in MongoDB Compass
// Drop a collection:
// Right-click collection name → Drop Collection → confirm

// Drop a database:
// Right-click database name → Drop Database → type database name → confirm

// ⚠️ There is no undo. Always backup production data before dropping.

CRUD at a glance — quick reference

The legacy methods insert(), update(), remove(), and findAndModify() still exist in older tutorials but are deprecated. Always prefer the explicit One/Many variants — they make intent clear and return useful metadata.

Real-life example: insertOne vs insertMany is like mailing one letter vs a bulk post — same post office, different batch size.

Practice every CRUD command in mongosh before writing driver code. Rishtaara's MongoDB MCQ at /mcq/mongodb-mcq tests these exact patterns.
Modern CRUD one-liners
use rishtaara

// CREATE
db.students.insertOne({ name: "Rahul", grade: 12 })
db.students.insertMany([{ name: "Priya", grade: 11 }, { name: "Vikram", grade: 10 }])

// READ
db.students.find({ grade: { $gte: 10 } })
db.students.findOne({ name: "Rahul" })

// UPDATE
db.students.updateOne({ name: "Rahul" }, { $set: { grade: 12, updatedAt: new Date() } })

// DELETE
db.students.deleteOne({ name: "Vikram" })
db.students.deleteMany({ active: false })

insertOne() — add a single document

insertOne() inserts one document into a collection. MongoDB auto-generates an _id field (ObjectId) if you omit it. The method returns an InsertOneResult with acknowledged and insertedId.

Real-life example: insertOne is like adding one new row to a spreadsheet — you get back the row ID MongoDB assigned.

Basic insertOne
use rishtaara

const result = db.products.insertOne({
  name: "Mechanical Keyboard",
  price: 3499,
  category: "electronics",
  tags: ["mechanical", "rgb"],
  stock: 45,
  createdAt: new Date()
})

// Result object:
// {
//   acknowledged: true,
//   insertedId: ObjectId("665a1b2c3d4e5f6789012345")
// }

// Retrieve by the returned ID
db.products.findOne({ _id: result.insertedId })
insertOne with custom _id
// Use a string _id when you have a natural key
db.users.insertOne({
  _id: "user_asha_khan",
  name: "Asha Khan",
  email: "asha@school.edu",
  role: "student"
})

// Use ObjectId explicitly
db.orders.insertOne({
  _id: ObjectId(),
  orderNumber: "ORD-2024-001",
  total: 1798,
  status: "pending"
})

insertMany() — batch inserts

insertMany() accepts an array of documents and inserts them in one round trip — much faster than looping insertOne in application code.

Real-life example: insertMany is like uploading a CSV of 500 products at once instead of clicking 'Add product' five hundred times.

Use ordered: false when importing large datasets where a few duplicate _id rows should not block the rest. Use ordered: true when every row must succeed or none should.
insertMany basics
db.products.insertMany([
  { name: "USB-C Hub", price: 1299, category: "electronics", stock: 80 },
  { name: "A5 Notebook", price: 79, category: "stationery", stock: 500 },
  { name: "Blue Pen Pack", price: 49, category: "stationery", stock: 1000 },
  { name: "Desk Organizer", price: 599, category: "furniture", stock: 30 }
])

// Return value:
// {
//   acknowledged: true,
//   insertedIds: {
//     '0': ObjectId("..."),
//     '1': ObjectId("..."),
//     ...
//   }
// }
Ordered vs unordered insertMany
// ORDERED (default: ordered: true)
// Stops at first error — remaining documents are NOT inserted
db.products.insertMany([
  { _id: 1, name: "Item A", price: 100 },
  { _id: 1, name: "Duplicate ID" },  // E11000 duplicate key — stops here
  { _id: 2, name: "Item C" }         // never attempted
], { ordered: true })

// UNORDERED (ordered: false)
// Continues after errors — inserts all non-conflicting docs
db.products.insertMany([
  { _id: 10, name: "Item X", price: 200 },
  { _id: 10, name: "Duplicate" },   // fails
  { _id: 11, name: "Item Y", price: 300 }  // still inserted
], { ordered: false })

// Use unordered for bulk seed scripts where partial success is OK

Legacy insert() — avoid in new code

The old insert() method accepted one document or an array. It is deprecated in favor of insertOne and insertMany. You may see it in legacy codebases — replace it when you touch that code.

Real-life example: insert() is like an old phone charger — it still works, but the new USB-C ports (insertOne/insertMany) are clearer and safer.

Legacy vs modern
// ❌ Legacy (deprecated)
db.students.insert({ name: "Old Style", grade: 9 })
db.students.insert([{ name: "A" }, { name: "B" }])

// ✅ Modern — always use these
db.students.insertOne({ name: "Modern Style", grade: 9 })
db.students.insertMany([{ name: "A" }, { name: "B" }])

Insert pitfalls — duplicate _id and 16 MB limit

Every document needs a unique _id within its collection. Inserting a duplicate _id throws error E11000 duplicate key. Each document has a maximum size of 16 MB — store large files in GridFS instead.

Real-life example: The 16 MB limit is like a mailbox that rejects packages over a certain weight — split big attachments into GridFS chunks.

  • Always set createdAt (and updatedAt) in application code for audit trails.
  • Use insertMany with ordered: false for resilient bulk imports.
  • Never store binary blobs larger than a few MB inside a document.
  • Check write concern for critical inserts — { w: 'majority' } on replica sets.
Duplicate _id error
db.students.insertOne({ _id: "student_001", name: "Asha" })
// Success

db.students.insertOne({ _id: "student_001", name: "Duplicate" })
// MongoServerError: E11000 duplicate key error collection:
// rishtaara.students index: _id_ dup key: { _id: "student_001" }

// Fix: omit _id and let MongoDB generate one, or use upsert on update
Document size and validation failures
// 16 MB max per document — includes all field names and values
// Bad: embedding a 20 MB base64 image
// Good: store file in GridFS or S3, keep URL in document

db.students.insertOne({
  name: "Rahul",
  grade: 12,
  profilePhotoUrl: "https://cdn.rishtaara.com/photos/rahul.jpg"
})

// If collection has JSON Schema validator, invalid docs are rejected:
db.createCollection("validated_students", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "grade"],
      properties: {
        name: { bsonType: "string" },
        grade: { bsonType: "int", minimum: 1, maximum: 12 }
      }
    }
  }
})

db.validated_students.insertOne({ name: "Test" })
// Fails — grade is required

updateOne() and updateMany()

Updates modify existing documents without replacing the entire document (unless you use replaceOne). updateOne changes the first match; updateMany changes every match. Both require update operators like $set — passing a plain object without operators replaces fields incorrectly in some contexts.

Real-life example: updateOne is like editing one student's grade in the register. updateMany is like raising every product price by 10% in one go.

updateOne — single document
use rishtaara

// Increase stock and stamp updatedAt
db.products.updateOne(
  { name: "Wireless Mouse" },
  {
    $set: { updatedAt: new Date() },
    $inc: { stock: 50 }
  }
)

// Return value:
// {
//   acknowledged: true,
//   matchedCount: 1,    // documents matching the filter
//   modifiedCount: 1,   // documents actually changed
//   upsertedCount: 0,
//   upsertedId: null
// }

// No match — matchedCount: 0, modifiedCount: 0
db.products.updateOne(
  { name: "Nonexistent Product" },
  { $set: { price: 999 } }
)
updateMany — all matches
// Apply 15% discount to all stationery
db.products.updateMany(
  { category: "stationery" },
  { $mul: { price: 0.85 }, $set: { onSale: true, updatedAt: new Date() } }
)

// Deactivate all students who graduated
db.students.updateMany(
  { grade: { $gt: 12 } },
  { $set: { active: false, graduatedAt: new Date() } }
)

// Add a tag to every electronics product
db.products.updateMany(
  { category: "electronics" },
  { $addToSet: { tags: "tech" } }
)

replaceOne() — full document swap

replaceOne() removes the existing document and inserts the replacement — except _id stays the same. Use only when you truly want to overwrite every field.

Real-life example: replaceOne is like tearing out a page and writing a completely new entry — not just correcting one line.

In 99% of production updates, use updateOne/updateMany with $set, $inc, and other operators. replaceOne is for full document replacement only.
replaceOne example
// Current document has many fields
db.settings.findOne({ key: "app_config" })
// { _id: ..., key: "app_config", theme: "dark", lang: "en", version: 3 }

// replaceOne overwrites ALL fields (except _id)
db.settings.replaceOne(
  { key: "app_config" },
  { key: "app_config", theme: "light", version: 4 }
)
// theme, lang changed; lang field is GONE unless you include it

// Prefer $set for partial updates in production
db.settings.updateOne(
  { key: "app_config" },
  { $set: { theme: "light", version: 4, updatedAt: new Date() } }
)

Upsert — update or insert

Setting { upsert: true } creates a new document when no match is found. Perfect for settings, counters, and idempotent sync operations.

Real-life example: Upsert is like 'update my seat assignment if I have one, otherwise create a new seat' — one operation handles both cases.

Upsert patterns
// Settings key-value store
db.settings.updateOne(
  { key: "maintenance_mode" },
  { $set: { value: false, updatedAt: new Date() } },
  { upsert: true }
)
// Creates { key: "maintenance_mode", value: false, ... } if missing

// User preference sync
db.user_prefs.updateOne(
  { userId: ObjectId("665a1b2c3d4e5f6789012345"), pref: "theme" },
  { $set: { value: "dark", updatedAt: new Date() } },
  { upsert: true }
)

// Return value when upsert creates:
// { matchedCount: 0, modifiedCount: 0, upsertedCount: 1, upsertedId: ObjectId("...") }

Legacy update() and intro to update operators

The legacy update() method is deprecated. Use updateOne, updateMany, or replaceOne instead. Update operators modify specific fields — detailed coverage of $set, $inc, $push, and more comes in the Update Operators module.

Real-life example: $set is like correcting one cell in a spreadsheet. $inc is like adding 10 to a stock count without reading the current value first.

  • Never pass a replacement document to updateOne without operators — use replaceOne if that is your intent.
  • upsert: true is powerful for config and counter collections.
  • $inc is atomic — safe for concurrent stock decrements.
  • Full operator reference ($push, $pull, $addToSet, etc.) is covered in the next module.
Legacy vs modern update
// ❌ Legacy (deprecated)
db.products.update({ name: "Mouse" }, { $set: { price: 799 } })
db.products.update({ category: "sale" }, { $set: { discounted: true } }, { multi: true })

// ✅ Modern
db.products.updateOne({ name: "Mouse" }, { $set: { price: 799 } })
db.products.updateMany({ category: "sale" }, { $set: { discounted: true } })
Quick intro — $set and $inc
// $set — assign or overwrite field values
db.students.updateOne(
  { name: "Asha Khan" },
  { $set: { grade: 11, "address.city": "Delhi", updatedAt: new Date() } }
)

// $inc — increment numeric fields (atomic)
db.products.updateOne(
  { name: "Wireless Mouse" },
  { $inc: { stock: -1, soldCount: 1 } }  // sell one unit
)

// Combine operators in one update
db.students.updateOne(
  { name: "Rahul Sharma" },
  {
    $set: { lastLogin: new Date() },
    $inc: { loginCount: 1 }
  }
)

deleteOne() and deleteMany()

deleteOne() removes the first document matching the filter. deleteMany() removes all matches. Both return deletedCount so you know how many documents were removed.

Real-life example: deleteOne is like removing one expired product from a shelf. deleteMany is like clearing every item tagged 'discontinued'.

deleteMany({}) wipes every document but keeps the collection and its indexes. drop() removes the collection entirely including indexes.
Delete single and multiple documents
use rishtaara

// Delete one product by name
const one = db.products.deleteOne({ name: "USB-C Hub" })
// { acknowledged: true, deletedCount: 1 }

// Delete all inactive students
const many = db.students.deleteMany({ active: false })
// { acknowledged: true, deletedCount: 42 }

// Delete with compound filter
db.orders.deleteMany({
  status: "cancelled",
  createdAt: { $lt: ISODate("2023-01-01T00:00:00Z") }
})

// No matches — deletedCount: 0 (not an error)
db.products.deleteOne({ name: "Does Not Exist" })
Delete entire collection contents
// Remove ALL documents in a collection (keeps indexes)
db.temp_logs.deleteMany({})

// Faster alternative — drops and recreates empty collection
db.temp_logs.drop()

// ⚠️ Never run deleteMany({}) on production users or orders
// without a backup and explicit approval

Delete database

To remove an entire database, switch to it and run db.dropDatabase(). This is irreversible without backups.

Real-life example: dropDatabase() is like demolishing a warehouse — everything inside is gone, not just emptying the shelves.

  • Atlas backups and point-in-time restore protect against accidental drops.
  • Use separate databases for dev, staging, and production.
  • Never expose dropDatabase permissions to application users.
Drop database in mongosh
// List databases first
show dbs

// Switch to the database you want to remove
use staging_rishtaara

// Confirm what you are about to destroy
show collections

// Drop it
db.dropDatabase()
// { ok: 1, dropped: "staging_rishtaara" }

// Production safety: restrict dropDatabase to admin roles only

Soft delete pattern — production best practice

Production apps rarely hard-delete user data. Instead, set a deletedAt timestamp or isDeleted flag and filter it out in every read query. This enables undo, audit trails, and GDPR-compliant delayed purges.

Real-life example: Soft delete is like moving a file to the Recycle Bin instead of permanently deleting it — you can restore it later if needed.

Rishtaara recommendation: soft delete for users, orders, and content. Hard delete only for temp data, sessions, and after legal retention periods.
Implement soft delete
// Soft delete a user
db.users.updateOne(
  { _id: ObjectId("665a1b2c3d4e5f6789012345") },
  {
    $set: {
      deletedAt: new Date(),
      active: false
    }
  }
)

// ALWAYS exclude soft-deleted in reads
db.users.find({
  deletedAt: { $exists: false },
  active: true
})

// Or explicitly check null
db.users.find({
  $or: [
    { deletedAt: { $exists: false } },
    { deletedAt: null }
  ]
})

// Restore (undo soft delete)
db.users.updateOne(
  { _id: ObjectId("665a1b2c3d4e5f6789012345") },
  { $unset: { deletedAt: "" }, $set: { active: true } }
)
TTL index for delayed hard delete
// Auto-purge soft-deleted records after 90 days
db.users.createIndex(
  { deletedAt: 1 },
  { expireAfterSeconds: 90 * 24 * 60 * 60 }
)

// MongoDB removes documents when deletedAt + 90 days passes
// Use for logs, sessions, and GDPR right-to-erasure workflows

find() and findOne() basics

find(filter) returns a cursor — an iterable handle to matching documents. findOne(filter) returns the first match as a single document or null. Both accept an optional projection as the second argument.

Real-life example: find() is like searching a library catalog for all books by an author. findOne() stops at the first match — like looking up one student by roll number.

Basic find and findOne
use rishtaara

// All products
db.products.find()

// Filter — implicit AND between fields
db.products.find({ category: "electronics", price: { $lt: 1000 } })

// findOne — single document or null
db.students.findOne({ name: "Asha Khan" })
// { _id: ObjectId("..."), name: "Asha Khan", grade: 10, ... }

db.students.findOne({ name: "Nobody" })
// null

// Equality shorthand — these are equivalent
db.products.find({ category: "electronics" })
db.products.find({ category: { $eq: "electronics" } })

Projection — return only what you need

Projection controls which fields appear in results. Use 1 to include and 0 to exclude. You cannot mix inclusion and exclusion except for _id.

Real-life example: Projection is like photocopying only the name and grade columns from a report — not the entire page.

In APIs, always project only fields the client needs. Returning full documents wastes bandwidth and may leak internal fields.
Include and exclude fields
// Include name and price only; hide _id
db.products.find(
  { category: "electronics" },
  { name: 1, price: 1, _id: 0 }
)

// Exclude heavy fields from all products
db.products.find(
  {},
  { description: 0, images: 0, reviews: 0 }
)

// findOne with projection
db.students.findOne(
  { grade: 12 },
  { name: 1, "address.city": 1, _id: 0 }
)

sort(), skip(), and limit()

Chain sort(), skip(), and limit() on a find cursor for ordered and paginated results. Sort values: 1 ascending, -1 descending.

Real-life example: sort + skip + limit is like flipping to page 3 of a sorted phone book — skip the first 20 names, show the next 10.

  • Always sort before skip/limit for consistent pagination.
  • Large skip values are slow — use range queries on _id or createdAt for deep pagination.
  • Compound indexes should match your sort fields for performance.
Sort and pagination
// Cheapest electronics first, top 5
db.products
  .find({ category: "electronics" })
  .sort({ price: 1 })
  .limit(5)

// Most expensive products
db.products.find().sort({ price: -1 }).limit(10)

// Pagination: page 3, 10 per page (skip first 20)
const page = 3
const pageSize = 10
db.products
  .find({ category: "stationery" })
  .sort({ name: 1 })
  .skip((page - 1) * pageSize)
  .limit(pageSize)

// Sort by multiple fields: grade ascending, then name ascending
db.students.find().sort({ grade: 1, name: 1 })

countDocuments()

countDocuments(filter) counts documents matching a filter — accurate but scans matching docs. estimatedDocumentCount() is fast but approximate and ignores filters.

Real-life example: countDocuments is like asking 'How many grade-10 students?' — exact count. estimatedDocumentCount is like glancing at a 'approximately 500 students' sign.

Count matching documents
// Exact count with filter
db.products.countDocuments({ category: "electronics" })
// 47

db.students.countDocuments({ grade: { $gte: 10 }, active: true })
// 156

// Count documents with a specific field present
db.students.countDocuments({ email: { $exists: true } })

// Fast approximate total (no filter allowed)
db.products.estimatedDocumentCount()
// ~500 (may differ slightly from true count)

// Pagination helper
const total = db.products.countDocuments({ category: "electronics" })
const totalPages = Math.ceil(total / 10)
print("Total pages:", totalPages)

Embedded and nested documents — dot notation

Documents can contain nested objects. Use dot notation to query nested fields — "address.city" matches the city inside an address object.

Real-life example: Dot notation is like writing 'home address → city' on a form — you drill into a nested level to find the value.

Query nested fields
// Sample student document:
// {
//   name: "Rahul Sharma",
//   address: { city: "Bengaluru", pincode: "560001", state: "Karnataka" },
//   guardian: { name: "Mr. Sharma", phone: "9876543210" }
// }

// Students in Bengaluru
db.students.find({ "address.city": "Bengaluru" })

// Students in Karnataka with pincode starting 560
db.students.find({
  "address.state": "Karnataka",
  "address.pincode": { $regex: "^560" }
})

// Products with supplier in a specific region
db.products.find({ "supplier.country": "IN" })

Query arrays — $all, $size, $elemMatch

Arrays store ordered lists. Equality on an array field matches if any element equals the value. Use $all, $size, and $elemMatch for precise array queries.

Real-life example: Querying arrays is like asking 'students who take BOTH Math AND CS' ($all) vs 'students who take Math' (any element match).

$elemMatch is required when multiple conditions must apply to the same array element. Without it, MongoDB may match conditions on different elements.
Basic array queries
// Any student with Mathematics as a subject
db.students.find({ subjects: "Mathematics" })

// $all — document must contain ALL listed values
db.students.find({
  subjects: { $all: ["Mathematics", "Computer Science"] }
})

// $size — array must have exactly N elements
db.products.find({ tags: { $size: 3 } })

// $elemMatch — at least one array element matches ALL conditions
db.orders.find({
  items: {
    $elemMatch: { productName: "Mouse", qty: { $gte: 2 } }
  }
})

// Match array of embedded documents
db.students.find({
  scores: {
    $elemMatch: { subject: "Physics", marks: { $gte: 90 } }
  }
})
Array position and nested arrays
// First element of array (zero-indexed)
db.students.find({ "subjects.0": "Mathematics" })

// Nested array in order items
db.orders.find({ "items.0.category": "electronics" })

// $size does not accept ranges — use $expr for "more than 3 tags"
db.products.find({
  $expr: { $gt: [{ $size: { $ifNull: ["$tags", []] } }, 3] }
})

Null and missing fields — $exists

In MongoDB, null and missing fields behave differently. { field: null } matches documents where field is null OR field does not exist. Use $exists to distinguish.

Real-life example: $exists is like asking 'Does this form have an email field filled in?' vs 'Is the email blank?' — different questions.

  • Use $exists: false in read queries when implementing soft delete filters.
  • Schema validation can require fields — but legacy data may still lack them.
  • Combine $exists with $type to find fields of unexpected types.
null vs missing with $exists
// Matches: field is null OR field is absent
db.students.find({ email: null })

// Field exists (any value including null)
db.students.find({ email: { $exists: true } })

// Field is completely absent
db.students.find({ email: { $exists: false } })

// Field exists AND is explicitly null (not missing)
db.students.find({
  email: { $exists: true, $eq: null }
})

// Students missing phone number
db.students.find({ phone: { $exists: false } })

// Products with no discount field (never on sale)
db.products.find({ discount: { $exists: false } })
Practical audit queries
// Find incomplete student profiles
db.students.find({
  $or: [
    { email: { $exists: false } },
    { email: null },
    { email: "" }
  ]
})

// Products missing required category
db.products.find({ category: { $exists: false } })

// Count how many users have no profile photo
db.users.countDocuments({ profilePhotoUrl: { $exists: false } })

Why find-and-modify?

Find-and-modify operations atomically find a document and apply a change in a single step. This prevents race conditions when multiple clients compete for the same document — counters, job queues, and inventory locks.

Real-life example: findOneAndUpdate is like a ticket counter that hands you the next number AND increments the counter in one motion — no two people get the same number.

  • findOneAndUpdate — find and apply update operators.
  • findOneAndReplace — find and replace entire document.
  • findOneAndDelete — find and remove.
  • Legacy findAndModify — deprecated, same family.

findOneAndUpdate and findOneAndReplace

findOneAndUpdate applies update operators. findOneAndReplace swaps the whole document. Both support upsert and returnDocument to control what gets returned.

Real-life example: returnDocument: 'after' is like seeing the receipt after the cashier updates your order — not the old version.

findOneAndUpdate — atomic counter
use rishtaara

// Auto-increment order ID (atomic)
const counter = db.counters.findOneAndUpdate(
  { _id: "order_seq" },
  { $inc: { seq: 1 } },
  {
    upsert: true,
    returnDocument: "after"   // "before" (default) or "after"
  }
)
// counter.seq is the new value — use for orderNumber

// Claim next pending job from a queue
const job = db.jobs.findOneAndUpdate(
  { status: "pending" },
  {
    $set: { status: "processing", claimedAt: new Date() }
  },
  {
    sort: { priority: -1, createdAt: 1 },
    returnDocument: "after"
  }
)
findOneAndReplace
// Replace entire config document
db.settings.findOneAndReplace(
  { key: "feature_flags" },
  {
    key: "feature_flags",
    darkMode: true,
    betaSearch: false,
    updatedAt: new Date()
  },
  { upsert: true, returnDocument: "after" }
)

// Prefer findOneAndUpdate with $set for partial changes
db.settings.findOneAndUpdate(
  { key: "feature_flags" },
  { $set: { betaSearch: true, updatedAt: new Date() } },
  { returnDocument: "after" }
)

findOneAndDelete and legacy findAndModify

findOneAndDelete atomically finds and removes a document — useful for queue consumers and pop-from-stack patterns. The legacy findAndModify command still works but prefer the modern methods.

Real-life example: findOneAndDelete is like taking the top item off a stack — you get the item and it is removed in one action.

returnDocument: 'before' returns the document as it was before the change. returnDocument: 'after' returns the updated document. Default is 'before'.
findOneAndDelete
// Pop oldest unprocessed notification
const notification = db.notifications.findOneAndDelete(
  { userId: ObjectId("665a1b2c3d4e5f6789012345"), read: false },
  { sort: { createdAt: 1 } }
)
// Returns the deleted document (before deletion)

// Process and remove expired session
const session = db.sessions.findOneAndDelete({
  token: "abc123xyz",
  expiresAt: { $gt: new Date() }
})

if (!session) {
  print("Session invalid or expired")
}
Legacy findAndModify vs modern API
// ❌ Legacy command (deprecated)
db.runCommand({
  findAndModify: "counters",
  query: { _id: "invoice_seq" },
  update: { $inc: { seq: 1 } },
  new: true,       // old name for returnDocument: "after"
  upsert: true
})

// ✅ Modern equivalent
db.counters.findOneAndUpdate(
  { _id: "invoice_seq" },
  { $inc: { seq: 1 } },
  { returnDocument: "after", upsert: true }
)

returnDocument — before vs after

Choose returnDocument based on what your application logic needs. Counters usually want 'after'. Audit logs may want 'before' to record the previous state.

Real-life example: 'before' is like a before-photo at the gym. 'after' is the progress photo — most apps want to show the new state.

  • Use sort with findOneAnd* when multiple documents match — controls which one is picked.
  • findOneAnd* methods are atomic on a single document — safe for concurrent workers.
  • In Mongoose: findOneAndUpdate maps to the same server command with { new: true } for 'after'.
Comparing before and after
db.products.insertOne({ name: "Test Item", stock: 100, price: 500 })

// BEFORE (default) — returns old document
const before = db.products.findOneAndUpdate(
  { name: "Test Item" },
  { $inc: { stock: -5 } },
  { returnDocument: "before" }
)
// before.stock === 100

// AFTER — returns updated document
const after = db.products.findOneAndUpdate(
  { name: "Test Item" },
  { $inc: { stock: -5 } },
  { returnDocument: "after" }
)
// after.stock === 95

$eq, $ne, $gt, $gte, $lt, $lte

Comparison operators filter documents by field values. Equality can be written as { field: value } or { field: { $eq: value } }. Range queries combine $gte and $lte on the same field.

Real-life example: Price filters on a shopping site — 'under ₹1000' is $lt: 1000, 'between ₹500 and ₹2000' is $gte plus $lte.

Range and equality filters
use rishtaara

// Products priced between ₹500 and ₹2000 (inclusive)
db.products.find({
  price: { $gte: 500, $lte: 2000 }
})

// Students above grade 10
db.students.find({ grade: { $gt: 10 } })

// Not equal — exclude discontinued
db.products.find({ status: { $ne: "discontinued" } })

// Exact match (explicit $eq)
db.students.find({ grade: { $eq: 10 } })
// Same as: db.students.find({ grade: 10 })
Student and product examples
// Students in grades 10, 11, or 12 with marks above 75
db.students.find({
  grade: { $gte: 10, $lte: 12 },
  "scores.overall": { $gt: 75 }
})

// Low-stock alert — products with stock under 20
db.products.find({
  stock: { $lt: 20 },
  category: { $ne: "discontinued" }
})

// Orders over ₹5000 placed this year
db.orders.find({
  total: { $gt: 5000 },
  createdAt: { $gte: ISODate("2024-01-01T00:00:00Z") }
})

$in and $nin — match lists

$in matches any value in an array. $nin matches none of the values — the negated form of $in.

Real-life example: $in is like a VIP list at a club — anyone on the list gets in. $nin is the banned list — those people are turned away.

$in and $nin
// Products in electronics or furniture
db.products.find({
  category: { $in: ["electronics", "furniture", "appliances"] }
})

// Students in grade 10 or 12 only
db.students.find({ grade: { $in: [10, 12] } })

// Exclude archived and discontinued categories
db.products.find({
  category: { $nin: ["archived", "discontinued", "internal"] }
})

// Orders with status NOT cancelled or refunded
db.orders.find({
  status: { $nin: ["cancelled", "refunded"] }
})
Combine $in with other operators
// Premium students: grade 11 or 12, active, in top cities
db.students.find({
  grade: { $in: [11, 12] },
  active: true,
  "address.city": { $in: ["Delhi", "Mumbai", "Bengaluru", "Chennai"] }
})

// Sale items in selected categories under ₹500
db.products.find({
  category: { $in: ["stationery", "electronics"] },
  price: { $lt: 500 },
  onSale: true
})

$cmp in aggregation — brief intro

$cmp compares two values and returns -1, 0, or 1 (like strcmp). It works inside aggregation expressions ($expr, $project, $addFields) — not as a top-level query operator on its own.

Real-life example: $cmp in aggregation is like a referee comparing two scores — who is higher, equal, or lower.

For simple field-to-value comparisons in find(), use $gt/$lt directly. Use $cmp inside $expr when comparing two fields in the same document.
$cmp in $expr and $project
// Products where salePrice is less than cost (selling at a loss)
db.products.find({
  $expr: { $eq: [{ $cmp: ["$salePrice", "$cost"] }, -1] }
  // $cmp returns -1 when salePrice < cost
})

// Add a comparison flag in aggregation
db.products.aggregate([
  {
    $project: {
      name: 1,
      price: 1,
      cost: 1,
      priceVsCost: {
        $switch: {
          branches: [
            { case: { $eq: [{ $cmp: ["$price", "$cost"] }, 1] }, then: "above_cost" },
            { case: { $eq: [{ $cmp: ["$price", "$cost"] }, 0] }, then: "at_cost" },
            { case: { $eq: [{ $cmp: ["$price", "$cost"] }, -1] }, then: "below_cost" }
          ],
          default: "unknown"
        }
      }
    }
  }
])

Practical patterns for Rishtaara apps

Comparison operators power every product filter, grade report, and date range query in Rishtaara courses. Combine them with indexes on filtered fields for production performance.

Real-life example: A 'Students scoring 90+' report is just { 'scores.overall': { $gte: 90 } } — one operator, instant results.

  • $ne cannot use an index efficiently in all cases — prefer $in with allowed values when possible.
  • Range queries ($gt, $lt) on the same field can use a single index range scan.
  • Date comparisons use ISODate or new Date() — always store dates as BSON Date type.
E-commerce and school report queries
// Flash sale: electronics under ₹999, in stock
db.products.find({
  category: "electronics",
  price: { $lte: 999 },
  stock: { $gt: 0 }
})

// Honor roll: grade 10+, overall score 85+
db.students.find({
  grade: { $gte: 10 },
  "scores.overall": { $gte: 85 },
  active: true
})

// Recent high-value orders
db.orders.find({
  total: { $gte: 10000 },
  createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) }
}).sort({ total: -1 })

$and, $or, $not, $nor

Logical operators combine multiple conditions. $and requires all conditions true. $or requires at least one. $not negates an expression. $nor matches documents where none of the conditions are true.

Real-life example: $or is like a menu combo — pizza OR pasta qualifies. $and is like needing both a ticket AND an ID to enter.

All four logical operators
use rishtaara

// $or — electronics OR furniture under ₹500
db.products.find({
  $or: [
    { category: "electronics", price: { $lt: 500 } },
    { category: "furniture", price: { $lt: 500 } }
  ]
})

// $and — explicit (usually implicit between top-level fields)
db.students.find({
  $and: [
    { active: true },
    { grade: { $in: [11, 12] } },
    { "address.city": "Bengaluru" }
  ]
})

// $not — negates the condition inside
db.products.find({
  price: { $not: { $gt: 1000 } }
})
// price <= 1000 OR price is non-numeric/null

// $nor — none of these conditions may be true
db.students.find({
  $nor: [
    { grade: { $lt: 9 } },
    { active: false }
  ]
})
// grade >= 9 AND active is not false

Implicit AND vs explicit $and

When you list multiple fields in a find filter, MongoDB applies implicit AND — every condition must match. Use explicit $and when you need multiple conditions on the same field or complex nesting.

Real-life example: Implicit AND is like a checklist where every box must be ticked. Explicit $and is needed when you check the same box twice with different rules.

Use parentheses-style nesting with $and and $or for readability. A query that is hard to read in code is hard to debug in production.
Implicit AND — most common
// Implicit AND — all three must match
db.products.find({
  category: "electronics",
  price: { $lt: 2000 },
  stock: { $gt: 0 }
})

// Equivalent explicit $and
db.products.find({
  $and: [
    { category: "electronics" },
    { price: { $lt: 2000 } },
    { stock: { $gt: 0 } }
  ]
})
When explicit $and is required
// Multiple conditions on the SAME field — need $and
db.products.find({
  $and: [
    { price: { $gte: 100 } },
    { price: { $lte: 500 } }
  ]
})
// Same as: db.products.find({ price: { $gte: 100, $lte: 500 } })

// Complex mix: (grade 10 OR 11) AND active AND in Delhi
db.students.find({
  $and: [
    { $or: [{ grade: 10 }, { grade: 11 }] },
    { active: true },
    { "address.city": "Delhi" }
  ]
})

// ⚠️ Use parentheses logic — $or binds less tightly than you might expect
// Always use explicit $and/$or for complex filters

Real-world filter combinations

Product search and student dashboards combine logical operators daily. Test each filter in mongosh, then copy to your API.

Real-life example: A 'Deals page' query is $or across categories plus $and for price and stock — marketing logic as database filters.

  • $nor is rarely used but useful for 'exclude if any of these match' logic.
  • $not wraps a single expression — different from $ne which compares to a value.
  • Index intersection can help $or queries — but compound indexes on $and patterns are more predictable.
E-commerce search filters
// Deals: (electronics under ₹1000) OR (stationery under ₹100), in stock
db.products.find({
  $and: [
    { stock: { $gt: 0 } },
    {
      $or: [
        { category: "electronics", price: { $lt: 1000 } },
        { category: "stationery", price: { $lt: 100 } }
      ]
    }
  ]
})

// Exclude clearance and archived unless on sale
db.products.find({
  $or: [
    { onSale: true },
    {
      $and: [
        { category: { $nin: ["clearance", "archived"] } },
        { status: { $ne: "hidden" } }
      ]
    }
  ]
})
Student dashboard queries
// Active students in Delhi or Mumbai, grade 10+
db.students.find({
  active: true,
  grade: { $gte: 10 },
  $or: [
    { "address.city": "Delhi" },
    { "address.city": "Mumbai" }
  ]
})

// Students missing email OR phone (incomplete profiles)
db.students.find({
  $or: [
    { email: { $exists: false } },
    { email: null },
    { phone: { $exists: false } }
  ]
})

Arithmetic in aggregation expressions

MongoDB arithmetic operators ($add, $subtract, $multiply, $divide, and others) work inside aggregation pipeline stages — primarily $project and $addFields. They compute new fields from existing values at query time.

Real-life example: Arithmetic operators are like Excel formulas — you calculate tax, totals, and margins on the fly without storing every derived value.

  • $add — sum values (also concatenates strings if one operand is a string).
  • $subtract — difference of two values.
  • $multiply — product of values.
  • $divide — quotient (returns null if dividing by zero).
  • $mod — remainder after division.
  • $abs, $ceil, $floor, $round, $trunc — rounding and absolute value.

$add, $subtract, $multiply, $divide

These four operators cover most business calculations — line totals, tax, discounts, and averages. Pass an array of expressions; field references use $fieldName syntax.

Real-life example: Order total = sum of (price × qty) for each line item — $multiply per item, then $add to sum.

Basic arithmetic in $addFields
use rishtaara

// Add computed fields to products
db.products.aggregate([
  {
    $addFields: {
      taxAmount: { $multiply: ["$price", 0.18] },
      priceWithTax: {
        $add: ["$price", { $multiply: ["$price", 0.18] }]
      },
      discountedPrice: {
        $subtract: [
          "$price",
          { $multiply: ["$price", { $divide: ["$discountPercent", 100] }] }
        ]
      }
    }
  },
  { $match: { category: "electronics" } },
  { $project: { name: 1, price: 1, priceWithTax: 1, discountedPrice: 1 } }
])
Order line calculations
db.orders.aggregate([
  { $unwind: "$items" },
  {
    $addFields: {
      "items.lineTotal": {
        $multiply: ["$items.price", "$items.qty"]
      }
    }
  },
  {
    $group: {
      _id: "$_id",
      orderNumber: { $first: "$orderNumber" },
      items: { $push: "$items" },
      subtotal: { $sum: { $multiply: ["$items.price", "$items.qty"] } }
    }
  },
  {
    $addFields: {
      tax: { $multiply: ["$subtotal", 0.18] },
      grandTotal: {
        $add: ["$subtotal", { $multiply: ["$subtotal", 0.18] }]
      }
    }
  }
])

$abs, $floor, $round, and related

Rounding operators clean up display values and bucket numeric data. $abs removes negative signs. $floor rounds down. $round accepts a place parameter.

Real-life example: $floor on a rating average is like truncating 4.7 stars to 4 for a simple display — no half-star graphics needed.

Rounding and absolute value
// Student score analysis
db.students.aggregate([
  {
    $project: {
      name: 1,
      grade: 1,
      scores: 1,
      avgScore: { $avg: "$scores.marks" },
      avgRounded: { $round: [{ $avg: "$scores.marks" }, 1] },
      avgFloored: { $floor: { $avg: "$scores.marks" } }
    }
  }
])

// Inventory adjustment — absolute value of stock change
db.inventory_logs.aggregate([
  {
    $project: {
      productName: 1,
      change: 1,
      absChange: { $abs: "$change" },
      direction: {
        $cond: {
          if: { $gte: ["$change", 0] },
          then: "restock",
          else: "sale"
        }
      }
    }
  }
])
Price bucketing with $floor and $divide
// Assign price tier: budget / mid / premium
db.products.aggregate([
  {
    $addFields: {
      priceTier: {
        $switch: {
          branches: [
            { case: { $lt: ["$price", 500] }, then: "budget" },
            { case: { $lt: ["$price", 2000] }, then: "mid" },
            { case: { $gte: ["$price", 2000] }, then: "premium" }
          ],
          default: "unknown"
        }
      },
      priceInThousands: {
        $divide: [{ $floor: "$price" }, 1000]
      }
    }
  },
  { $sort: { price: 1 } }
])

Examples in $project and $addFields

$project shapes output and can compute fields. $addFields adds new fields while keeping all existing ones. Choose $project when you want a slim response; $addFields when you need originals plus computed values.

Real-life example: $addFields is like adding a 'Total' column to a spreadsheet without hiding the original columns. $project is like exporting only selected columns.

Divide by zero returns null in MongoDB — handle with $cond or $ifNull in production pipelines. Store critical computed values at write time if reads are hot-path.
$project — computed report columns
// Product margin report
db.products.aggregate([
  {
    $project: {
      name: 1,
      category: 1,
      price: 1,
      cost: 1,
      margin: { $subtract: ["$price", "$cost"] },
      marginPercent: {
        $round: [
          {
            $multiply: [
              {
                $divide: [
                  { $subtract: ["$price", "$cost"] },
                  "$cost"
                ]
              },
              100
            ]
          },
          2
        ]
      },
      _id: 0
    }
  },
  { $match: { margin: { $gt: 0 } } },
  { $sort: { marginPercent: -1 } }
])
$addFields — enrich then filter
// Student age from birthYear (computed at query time)
db.students.aggregate([
  {
    $addFields: {
      age: { $subtract: [2024, "$birthYear"] },
      subjectCount: { $size: { $ifNull: ["$subjects", []] } },
      scoreGap: {
        $subtract: [100, { $ifNull: ["$scores.overall", 0] }]
      }
    }
  },
  {
    $match: {
      age: { $gte: 15 },
      "scores.overall": { $lt: 60 }
    }
  },
  {
    $project: {
      name: 1,
      grade: 1,
      age: 1,
      scoreGap: 1,
      needsSupport: {
        $cond: { if: { $gte: ["$scoreGap", 20] }, then: true, else: false }
      }
    }
  }
])

Field Update Operators — overview

When you update a MongoDB document, you rarely replace the whole document. Instead you use field update operators — instructions that say 'change this field, leave everything else alone.'

All field update operators start with $. They go inside the second argument of updateOne, updateMany, or findOneAndUpdate.

Real-life example: Field update operators are like sticky notes on a student file — 'raise grade to 11', 'add ₹50 to wallet balance' — without rewriting the entire file from scratch.

  • $set — assign or overwrite a field value
  • $unset — remove a field entirely
  • $inc — add or subtract a number
  • $mul — multiply a number field
  • $min / $max — keep the smaller or larger value
  • $rename — change a field name
Never pass a plain object like { price: 999 } in an update — that replaces the whole document. Always wrap changes in operators like { $set: { price: 999 } }.

$set — the workhorse

$set assigns a value to a field. If the field does not exist, MongoDB creates it. You can set nested fields with dot notation.

Real-life example: $set is like filling in a blank on a form — name, email, or nested address.city — without touching other boxes.

$set on nested fields and mixed types
// Update a student's grade and city
db.students.updateOne(
  { email: "asha@rishtaara.edu" },
  {
    $set: {
      grade: 11,
      "address.city": "Mumbai",
      updatedAt: new Date()
    }
  }
)

// Set a new field on a product
db.products.updateOne(
  { sku: "MOUSE-01" },
  { $set: { featured: true, badge: "Editor's Choice" } }
)

$unset — remove fields

$unset deletes a field from a document. Pass an empty string, 1, or true as the value — the value itself is ignored; only the field name matters.

Real-life example: $unset is like redacting a line from a record — the rest of the document stays intact.

Remove deprecated or sensitive fields
// Remove a temporary promo flag after sale ends
db.products.updateMany(
  { promoEndsAt: { $lt: new Date() } },
  { $unset: { promoCode: "", discountPercent: "" } }
)

// GDPR-style: remove optional phone if user opts out
db.users.updateOne(
  { _id: userId },
  { $unset: { phone: "" } }
)

$inc and $mul — numeric changes

$inc adds (or subtracts with a negative number) to a numeric field. If the field is missing, MongoDB treats it as zero before applying the increment.

$mul multiplies a field by a number. Useful for percentage discounts or currency conversion.

Real-life example: $inc is a shop till — each sale adds +1 to stock sold and −1 from inventory. $mul is a 10% off sticker that multiplies price by 0.9.

$inc only works on numeric fields. Trying to $inc a string field throws an error.
Inventory and loyalty points
// Customer buys 2 units — decrease stock, bump sales counter
db.products.updateOne(
  { sku: "NOTEBOOK-A5" },
  {
    $inc: { stock: -2, unitsSold: 2 },
    $set: { updatedAt: new Date() }
  }
)

// Apply 15% discount to all clearance items
db.products.updateMany(
  { category: "clearance" },
  { $mul: { price: 0.85 } }
)

// Add loyalty points after order
db.users.updateOne(
  { _id: userId },
  { $inc: { loyaltyPoints: 50 } }
)

$min and $max — keep extremes

$max updates a field only if the new value is greater than the current value. $min updates only if the new value is smaller.

These are perfect for leaderboards, high-water marks, and price floors without a read-modify-write race.

Real-life example: $max on a high score is like a trophy case — only a better score replaces what's on display. $min on price is a 'never below ₹99' rule.

Leaderboard and price floor
// Only update if new score beats the old one
db.players.updateOne(
  { username: "RahulX" },
  { $max: { highScore: 12500 } }
)

// Ensure sale price never drops below cost floor
db.products.updateOne(
  { sku: "LAMP-LED" },
  { $min: { salePrice: 1499 } }
)

// Track lowest recorded temperature for a sensor
db.sensors.updateOne(
  { sensorId: "TEMP-01" },
  { $min: { minCelsius: -2.4 } }
)

$rename — change field names

$rename moves data from one field name to another. If the target name already exists, $rename overwrites it.

Use during schema migrations when you rename phone to mobile across many documents.

Real-life example: $rename is relabelling folders in a filing cabinet — 'phone' becomes 'mobile' without copying the papers by hand.

On Rishtaara practice projects, always set updatedAt with $set whenever you $inc stock or change prices — your future self will thank you during debugging.
Schema migration with $rename
// Rename field across all users
db.users.updateMany(
  {},
  { $rename: { phone: "mobile" } }
)

// Combine rename with $set for a clean migration
db.students.updateMany(
  { legacyId: { $exists: true } },
  {
    $rename: { legacyId: "externalId" },
    $set: { migratedAt: new Date() }
  }
)

Array expressions in aggregation

Array expression operators work inside aggregation pipelines — in $project, $addFields, $group accumulators, and $match with $expr. They inspect and transform arrays without changing stored documents.

Real-life example: These operators are like a teacher counting how many subjects a student takes, reading the first subject on the list, or merging two class rosters — all in one report query.

  • $isArray — true if the value is an array
  • $size — number of elements (must know array size at query time)
  • $arrayElemAt — pick element by index (0-based)
  • $concatArrays — join two or more arrays
  • $reverseArray — reverse element order

$isArray and $size

$isArray returns a boolean — useful when documents might store a single value or an array depending on age of the schema.

$size returns the element count. It errors if the field is not an array — pair it with $isArray in conditional logic.

Real-life example: $size is counting items in a shopping cart before checkout — 'does this order have exactly 3 line items?'

$size in a find() filter requires an exact count — it cannot mean 'at least 3'. For that, use $expr with $gt and $size, or store itemCount denormalized.
Validate and count arrays in $project
db.orders.aggregate([
  {
    $project: {
      orderId: 1,
      itemCount: { $size: "$items" },
      tagsIsArray: { $isArray: "$tags" },
      hasManyItems: { $gt: [{ $size: "$items" }, 5] }
    }
  }
])

// Find orders with exactly 1 item (using $expr in find)
db.orders.find({
  $expr: { $eq: [{ $size: "$items" }, 1] }
})

$arrayElemAt — index into an array

$arrayElemAt takes an array and a zero-based index. Negative indices count from the end: −1 is the last element.

Real-life example: $arrayElemAt is grabbing the first name on a waitlist or the most recent entry in a log array.

First and last elements in a pipeline
db.students.aggregate([
  {
    $project: {
      name: 1,
      primarySubject: { $arrayElemAt: ["$subjects", 0] },
      latestGrade: { $arrayElemAt: ["$grades", -1] }
    }
  }
])

// Safe access when array might be empty — use $ifNull
db.students.aggregate([
  {
    $addFields: {
      topSubject: {
        $ifNull: [
          { $arrayElemAt: ["$subjects", 0] },
          "Undeclared"
        ]
      }
    }
  }
])

$concatArrays and $reverseArray

$concatArrays joins arrays in order — like stapling two lists together.

$reverseArray flips order — handy for 'newest first' display without sorting objects.

Real-life example: $concatArrays merges 'morning batch' and 'evening batch' student lists into one attendance sheet.

Merge tags and reverse activity feed
db.products.aggregate([
  {
    $project: {
      name: 1,
      allTags: {
        $concatArrays: [
          { $ifNull: ["$tags", []] },
          { $ifNull: ["$autoTags", []] }
        ]
      }
    }
  },
  {
    $project: {
      name: 1,
      allTags: 1,
      tagsNewestFirst: { $reverseArray: "$allTags" }
    }
  }
])

// Combine with $setUnion for unique tags (bonus)
db.products.aggregate([
  {
    $addFields: {
      uniqueTags: { $setUnion: ["$tags", "$autoTags"] }
    }
  }
])

Practical report: cart summary

Array expressions shine when embedded arrays hold line items, grades, or event history. Compute derived fields in the database instead of looping in Node.js.

Real-life example: A Rishtaara canteen app stores each student's weekly orders as an array — one aggregation returns total snacks bought without loading every document into the server.

Sum line items using array expressions
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $addFields: {
      lineCount: { $size: "$items" },
      firstItemName: { $arrayElemAt: ["$items.name", 0] }
    }
  },
  {
    $group: {
      _id: "$userId",
      orderCount: { $sum: 1 },
      totalLines: { $sum: "$lineCount" }
    }
  },
  { $sort: { totalLines: -1 } },
  { $limit: 10 }
])

Updating arrays in place

Documents often store lists — tags, cart items, grades, permissions. Array update operators modify those lists without replacing the whole document.

Real-life example: Array updates are like editing a guest list — add a name, remove someone who cancelled, or change one person's seat without reprinting the whole list.

  • $push — append values to an array
  • $addToSet — append only if not already present
  • $pull — remove all matching values
  • $pullAll — remove listed values
  • $pop — remove first or last element
  • Positional $ and $[] — update matched array elements

$push and $addToSet

$push adds one or more elements to an array. Use $each to push many at once, $sort to order, and $slice to cap length (useful for 'last 50 messages').

$addToSet only inserts if the value is not already in the array — ideal for tags and unique IDs.

Real-life example: $push is adding a book to a library checkout list. $addToSet is adding a hashtag — duplicates are ignored.

Push, capped history, and unique tags
// Add a subject to a student
db.students.updateOne(
  { email: "rahul@rishtaara.edu" },
  { $push: { subjects: "Economics" } }
)

// Push multiple + keep only last 20 log entries
db.users.updateOne(
  { _id: userId },
  {
    $push: {
      activityLog: {
        $each: [{ action: "login", at: new Date() }],
        $sort: { at: -1 },
        $slice: 20
      }
    }
  }
)

// Tag without duplicates
db.products.updateOne(
  { sku: "MOUSE-01" },
  { $addToSet: { tags: { $each: ["wireless", "bestseller", "wireless"] } } }
)

$pull, $pullAll, and $pop

$pull removes every array element that matches a condition (or exact value).

$pullAll removes all occurrences of listed values.

$pop removes one end: −1 removes the last element, 1 removes the first.

Real-life example: $pull is striking cancelled items off an order. $pop is removing the most recently added item from a stack.

Remove from arrays
// Remove "clearance" tag everywhere it appears
db.products.updateOne(
  { sku: "MOUSE-01" },
  { $pull: { tags: "clearance" } }
)

// Pull objects matching a condition
db.orders.updateOne(
  { _id: orderId },
  { $pull: { items: { qty: 0 } } }
)

// Remove several status flags at once
db.tasks.updateMany(
  {},
  { $pullAll: { flags: ["draft", "archived"] } }
)

// Remove last chat message
db.rooms.updateOne(
  { roomId: "general" },
  { $pop: { messages: 1 } }
)

Positional $ — update one matched element

When your filter matches a document and an array element, the positional $ operator updates the first matching array element.

The filter must include the array field so MongoDB knows which element matched — e.g. { 'items.sku': 'ABC' } with { $set: { 'items.$.qty': 3 } }.

Real-life example: Positional $ is updating one student's grade in a class roster when you know their roll number — not everyone's grade.

Positional $ only updates the first match. If multiple array elements match, use $[] or arrayFilters (covered next).
Update first matching array element
// Bump quantity for one line item in a cart
db.carts.updateOne(
  { userId: userId, "items.sku": "NOTEBOOK-A5" },
  {
    $inc: { "items.$.qty": 1 },
    $set: { "items.$.updatedAt": new Date() }
  }
)

// Mark one notification as read
db.users.updateOne(
  { _id: userId, "notifications.id": notifId },
  { $set: { "notifications.$.read": true } }
)

All positional $[] and arrayFilters

$[] updates every element in an array when the filter matches the document — no per-element filter in the query.

arrayFilters (update option) gives named placeholders like $[elem] for fine-grained multi-element updates.

Real-life example: $[] is marking every item in a shipment as 'in transit'. arrayFilters is updating only items where status is 'pending'.

$[] and arrayFilters
// Discount every line item in matching orders
db.orders.updateMany(
  { status: "open" },
  { $mul: { "items.$[].price": 0.9 } }
)

// Only update items that are still pending
db.orders.updateMany(
  { _id: orderId },
  { $set: { "items.$[line].status": "shipped" } },
  { arrayFilters: [{ "line.status": "pending" }] }
)

// $elemMatch in query + positional $ for nested match
db.students.updateOne(
  {
    name: "Asha",
    grades: { $elemMatch: { subject: "Math", score: { $lt: 40 } } }
  },
  { $set: { "grades.$.remark": "Remedial class required" } }
)

String operators in aggregation

String expression operators format and compare text inside aggregation pipelines. They do not change stored documents unless you write results back with $merge or an update.

Real-life example: These operators are like a mail-merge tool — combine first and last name, uppercase a coupon code, or compare two emails case-insensitively.

  • $concat — join strings
  • $toUpper / $toLower — change case
  • $strcasecmp — compare two strings (case-insensitive, returns −1/0/1)
  • $substrCP — slice by Unicode code points (safe for emoji and Devanagari)

$concat, $toUpper, and $toLower

$concat joins any number of strings and coerces non-strings where possible.

$toUpper and $toLower normalize case for display or matching.

Real-life example: $concat builds a display label 'Asha Kumar — Grade 10' from separate fields for a Rishtaara report card PDF.

Build display names and normalize codes
db.students.aggregate([
  {
    $project: {
      fullLabel: {
        $concat: ["$firstName", " ", "$lastName", " — Grade ", { $toString: "$grade" }]
      },
      emailLower: { $toLower: "$email" },
      couponNormalized: { $toUpper: "$couponCode" }
    }
  }
])

// Mask part of a phone for admin UI
db.users.aggregate([
  {
    $addFields: {
      phoneMasked: {
        $concat: [
          { $substrCP: ["$phone", 0, 2] },
          "****",
          { $substrCP: ["$phone", 6, 4] }
        ]
      }
    }
  }
])

$strcasecmp — compare without case

$strcasecmp returns 0 if strings are equal ignoring case, −1 if first is less, 1 if first is greater (lexicographic).

Prefer $strcasecmp over $toLower equality when sorting or branching in $cond.

Real-life example: $strcasecmp checks whether two signup emails are the same even if one user typed ASHA@Gmail.com.

Duplicate email detection in pipeline
db.signups.aggregate([
  {
    $addFields: {
      emailNormalized: { $toLower: "$email" }
    }
  },
  {
    $group: {
      _id: "$emailNormalized",
      count: { $sum: 1 },
      names: { $push: "$name" }
    }
  },
  { $match: { count: { $gt: 1 } } }
])

// Branch on string compare
db.products.aggregate([
  {
    $addFields: {
      tier: {
        $cond: {
          if: { $eq: [{ $strcasecmp: ["$category", "premium"] }, 0] },
          then: "A",
          else: "B"
        }
      }
    }
  }
])

$substrCP — safe substring

$substrCP(start, length) slices by Unicode code points — unlike legacy $substr which broke multi-byte characters.

Use it for initials, SKU prefixes, and fixed-width codes.

Real-life example: $substrCP takes the first two characters of a city name for a sort key — works for 'Mumbai' and emoji-rich social handles alike.

For full-text search in production, combine text indexes with $text or Atlas Search — string expressions are for formatting and light matching in pipelines.
Initials and SKU prefix reports
db.students.aggregate([
  {
    $project: {
      name: 1,
      initials: {
        $concat: [
          { $substrCP: ["$firstName", 0, 1] },
          ".",
          { $substrCP: ["$lastName", 0, 1] },
          "."
        ]
      },
      cityCode: { $substrCP: ["$address.city", 0, 3] }
    }
  }
])

// Group sales by SKU prefix (first 4 chars)
db.sales.aggregate([
  {
    $group: {
      _id: { $substrCP: ["$sku", 0, 4] },
      revenue: { $sum: "$amount" }
    }
  },
  { $sort: { revenue: -1 } }
])

What is aggregation?

Aggregation is MongoDB's analytics engine. Documents flow through a pipeline of stages — each stage transforms the stream and passes results to the next.

Think SQL: $match ≈ WHERE, $group ≈ GROUP BY, $sort ≈ ORDER BY, $project ≈ SELECT columns, $lookup ≈ JOIN.

Real-life example: An aggregation pipeline is a factory assembly line — raw orders enter, get filtered, grouped, labelled, and boxed reports come out the end.

Sample aggregation pipeline

Your first pipeline: $match → $group → $sort

Start every analytics query by trimming documents early with $match — less data for later stages means faster runs.

$group collapses rows by a key (_id) and computes totals with accumulators like $sum and $avg.

Real-life example: A Rishtaara shop owner asks 'Which category sold the most this month?' — three stages answer that without exporting CSV files.

Revenue by category this month
db.orders.aggregate([
  // Stage 1: only completed orders in July
  {
    $match: {
      status: "completed",
      createdAt: {
        $gte: ISODate("2025-07-01"),
        $lt: ISODate("2025-08-01")
      }
    }
  },
  // Stage 2: one row per category
  {
    $group: {
      _id: "$category",
      totalRevenue: { $sum: "$total" },
      orderCount: { $sum: 1 },
      avgOrder: { $avg: "$total" }
    }
  },
  // Stage 3: bestsellers first
  { $sort: { totalRevenue: -1 } }
])

Pipeline mental model

Each stage receives documents from the previous stage and outputs a new stream. Order matters: $match before $group is almost always cheaper than grouping first.

The aggregation cursor returns batches — use allowDiskUse: true for heavy pipelines that might exceed memory limits.

Real-life example: Putting $match first is like filtering rice before cooking — you do not boil the whole sack then throw away stones.

  • Filter early with $match near the start
  • Shape output with $project or $addFields after computing fields
  • Use $limit / $skip for pagination after $sort
  • Test stages one at a time in mongosh — comment out later stages while debugging
On Rishtaara, run db.orders.explain('executionStats').aggregate([...]) to see whether your $match uses an index.

Updates with aggregation pipeline

Since MongoDB 4.2, updateOne and updateMany accept an aggregation pipeline as the update body — not just $set/$inc objects.

This lets you compute new field values from existing fields, conditionally branch, and reshape arrays in one atomic update.

Real-life example: Pipeline updates are a smart cash register — it reads old price, applies tax rules, and writes new totals in one transaction.

Conditional update with pipeline syntax
// Recalculate line totals and order total from items array
db.orders.updateOne(
  { _id: orderId },
  [
    {
      $set: {
        items: {
          $map: {
            input: "$items",
            as: "line",
            in: {
              $mergeObjects: [
                "$$line",
                {
                  subtotal: {
                    $multiply: ["$$line.qty", "$$line.price"]
                  }
                }
              ]
            }
          }
        }
      }
    },
    {
      $set: {
        total: { $sum: "$items.subtotal" },
        updatedAt: new Date()
      }
    }
  ]
)

// Promote to VIP when lifetime spend crosses threshold
db.users.updateMany(
  { lifetimeSpend: { $gte: 50000 } },
  [
    {
      $set: {
        tier: "VIP",
        perks: { $concatArrays: [{ $ifNull: ["$perks", []] }, ["free_shipping"]] }
      }
    }
  ]
)

Key aggregation stages — overview

Stages are the building blocks of every pipeline. Knowing when to use each stage separates slow reports from snappy dashboards.

Real-life example: Stages are kitchen stations — prep ($match), cook ($group), plate ($project), serve ($limit).

  • $match — filter documents (put early)
  • $group — aggregate by _id key
  • $project — include/exclude/compute fields
  • $unwind — one document per array element
  • $sort — order results
  • $limit / $skip — pagination
  • $facet — multiple sub-pipelines in one query
  • $bucket — histogram-style grouping by ranges

$match, $project, $unwind

$unwind duplicates the parent document for each array element — essential before grouping line items.

Real-life example: $unwind is exploding a combo meal into individual items so you can count how many fries were sold.

Filter, reshape, flatten arrays
db.orders.aggregate([
  { $match: { status: "completed", total: { $gte: 500 } } },
  { $unwind: "$items" },
  {
    $project: {
      orderId: 1,
      sku: "$items.sku",
      lineTotal: { $multiply: ["$items.qty", "$items.price"] },
      _id: 0
    }
  }
])

$sort, $limit, $skip

Large $skip values get slow — for deep pagination prefer range queries on sort keys or $search / bookmark tokens.
Top 5 customers — page 2
const page = 2;
const pageSize = 5;

db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$userId", spend: { $sum: "$total" } } },
  { $sort: { spend: -1 } },
  { $skip: (page - 1) * pageSize },
  { $limit: pageSize }
])

$facet and $bucket

$facet runs several sub-pipelines on the same input — one round trip for 'summary + detail + chart data'.

$bucket groups numeric values into ranges — price histograms, age bands, score tiers.

Real-life example: $facet is one dashboard API call returning both KPI cards and the table beneath them.

Dashboard in one aggregation
db.products.aggregate([
  { $match: { active: true } },
  {
    $facet: {
      totals: [
        {
          $group: {
            _id: null,
            count: { $sum: 1 },
            avgPrice: { $avg: "$price" }
          }
        }
      ],
      byCategory: [
        { $group: { _id: "$category", count: { $sum: 1 } } },
        { $sort: { count: -1 } }
      ],
      priceHistogram: [
        {
          $bucket: {
            groupBy: "$price",
            boundaries: [0, 500, 1000, 2500, 5000, 100000],
            default: "5000+",
            output: { count: { $sum: 1 } }
          }
        }
      ]
    }
  }
])

Pipeline limits and performance

Aggregation stages buffer documents in memory. By default, a stage may use up to 100 MB of RAM unless allowDiskUse: true lets MongoDB spill to disk.

Each document passing through a pipeline must stay under the 16 MB BSON document size limit — watch $push / $group arrays that grow without bound.

MongoDB limits pipeline complexity: maximum 1000 stages (you will never hit this), but heavy $lookup and unindexed $match on huge collections hurt in production.

Real-life example: Hitting memory limits is like cramming every invoice ever onto one desk — allowDiskUse moves overflow papers to a filing cabinet, but it is slower.

  • allowDiskUse: true — for large analytics jobs (batch reports, ETL)
  • Index fields used in early $match and $sort
  • $limit early when you only need top N after $sort
  • Atlas — use aggregation explain and Performance Advisor for slow pipelines
  • 16 MB cap — $group with massive arrays may fail; pre-aggregate or $limit inside $group
Run with allowDiskUse
db.events.aggregate(
  [
    { $match: { ts: { $gte: ISODate("2024-01-01") } } },
    { $group: { _id: "$userId", events: { $push: "$type" } } }
  ],
  { allowDiskUse: true }
)

Stages that output, join, and count

Some stages do more than transform — they write results ($out), count documents ($count), join collections ($lookup), or accumulate in $group.

Real-life example: $lookup is HR pulling employee names from the staff directory when payroll only stored employee IDs.

  • $lookup — left outer join between collections
  • $group + accumulators — $sum, $avg, $first, $last, $max, $min
  • $count — shortcut for { $group: { _id: null, n: { $sum: 1 } } }
  • $out — write pipeline results to a collection

$lookup — join two collections

$lookup runs a sub-pipeline on the foreign collection (modern syntax) or matches localField to foreignField (classic syntax).

Results land in a new array field — usually followed by $unwind when you expect one match.

Real-life example: Orders store userId; users store name and email — $lookup attaches customer details to each order for a support dashboard.

Frequent $lookup on hot read paths is a smell — consider embedding customer name on the order at write time for checkout APIs.
Classic and pipeline $lookup
// Classic: orders → users
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $lookup: {
      from: "users",
      localField: "userId",
      foreignField: "_id",
      as: "customer"
    }
  },
  { $unwind: "$customer" },
  {
    $project: {
      orderId: 1,
      total: 1,
      customerName: "$customer.name",
      customerEmail: "$customer.email"
    }
  }
])

// Pipeline $lookup — join with extra filter on users
db.orders.aggregate([
  {
    $lookup: {
      from: "users",
      let: { uid: "$userId" },
      pipeline: [
        { $match: { $expr: { $eq: ["$_id", "$$uid"] } } },
        { $project: { name: 1, tier: 1 } }
      ],
      as: "customer"
    }
  }
])

$group accumulators — $first and friends

Inside $group, accumulators reduce many documents to one value per _id key.

$first and $last capture values after a $sort — common for 'latest order per user'.

Real-life example: $first after sorting by date gives you each student's most recent exam score in one row.

Latest order per customer
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $sort: { userId: 1, createdAt: -1 } },
  {
    $group: {
      _id: "$userId",
      latestOrderId: { $first: "$_id" },
      latestTotal: { $first: "$total" },
      latestDate: { $first: "$createdAt" },
      orderCount: { $sum: 1 },
      lifetimeSpend: { $sum: "$total" }
    }
  },
  { $sort: { lifetimeSpend: -1 } },
  { $limit: 20 }
])

$count and $out

$count is sugar for counting documents at that pipeline point — great after $match.

$out replaces a collection with pipeline output — use for materialized views and nightly ETL jobs (not mid-request in user-facing APIs).

Real-life example: $out is printing yesterday's sales summary into a fresh binder labelled daily_summary — readers grab the binder instead of rerunning the whole calculation.

Count matching docs and materialize a report
// Quick count without fetching rows
db.orders.aggregate([
  { $match: { status: "pending", createdAt: { $lt: new Date(Date.now() - 86400000) } } },
  { $count: "stalePendingOrders" }
])

// Nightly: build summary collection for BI tools
db.orders.aggregate([
  { $match: { status: "completed" } },
  {
    $group: {
      _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
      revenue: { $sum: "$total" },
      orders: { $sum: 1 }
    }
  },
  { $sort: { _id: 1 } },
  { $out: "daily_revenue_summary" }
])

Practical join + group: category leaderboard

Combine $lookup, $unwind, $group, and $sort for reports that would take many SQL joins and subqueries.

Real-life example: Rishtaara marketplace — show top sellers per category with shop name and total GMV for the admin homepage.

Seller leaderboard with $lookup
db.order_items.aggregate([
  { $match: { createdAt: { $gte: ISODate("2025-01-01") } } },
  {
    $lookup: {
      from: "sellers",
      localField: "sellerId",
      foreignField: "_id",
      as: "seller"
    }
  },
  { $unwind: "$seller" },
  {
    $group: {
      _id: { category: "$category", sellerId: "$sellerId" },
      shopName: { $first: "$seller.shopName" },
      gmv: { $sum: { $multiply: ["$qty", "$price"] } },
      units: { $sum: "$qty" }
    }
  },
  { $sort: { "_id.category": 1, gmv: -1 } },
  {
    $group: {
      _id: "$_id.category",
      topSeller: { $first: "$shopName" },
      topGmv: { $first: "$gmv" }
    }
  }
])

Why indexes matter

Without an index, MongoDB performs a collection scan (COLLSCAN) — reading every document. On millions of rows, that turns milliseconds into seconds.

Indexes are B-tree structures keyed by field values. The query planner chooses IXSCAN when an index supports your filter and sort.

Real-life example: An index is the alphabetized index at the back of a textbook — you jump to page 214 instead of flipping every page.

Index vs collection scan

createIndex — define access paths

createIndex builds an index in the background on standalone instances (check build progress for large collections).

Direction 1 is ascending, −1 descending — important for sort compatibility on compound indexes.

Real-life example: Indexing email is like sorting the student roster by email — lookups by email become instant.

Common createIndex patterns
// Single field — login by email
db.users.createIndex({ email: 1 })

// Compound — user's orders newest first
db.orders.createIndex({ userId: 1, createdAt: -1 })

// Unique — prevent duplicate SKUs
db.products.createIndex({ sku: 1 }, { unique: true })

// Named index (easier to drop later)
db.students.createIndex(
  { grade: 1, "address.city": 1 },
  { name: "grade_city_idx" }
)

getIndexes and dropIndex

getIndexes lists index name, keys, and options — always check before dropping.

dropIndex takes the index name (not the field list). dropIndexes drops all but _id.

Real-life example: getIndexes is inventory of every shortcut file in the cabinet; dropIndex removes one outdated shortcut you no longer need.

Every index speeds reads but slows writes — do not index every field. Index what your slow queries actually filter and sort on.
Inspect and remove indexes
// List all indexes on a collection
db.products.getIndexes()

// Drop by name
db.products.dropIndex("sku_1")

// Drop all custom indexes (keeps _id_)
db.products.dropIndexes()

explain() — COLLSCAN vs IXSCAN

explain('executionStats') shows the winning plan, docs examined, and execution time — run this on every production query you care about.

IXSCAN means index scan (good at scale). COLLSCAN means full collection scan (fine for tiny collections, dangerous on large ones).

Real-life example: explain() is a shop security camera replay — you see whether the clerk checked the indexed ledger or searched every box in the warehouse.

Compare plans before and after indexing
// Before index — likely COLLSCAN
db.orders.find({ userId: ObjectId("..."), status: "pending" })
  .sort({ createdAt: -1 })
  .explain("executionStats")

// Create supporting index
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })

// After index — look for IXSCAN
db.orders.find({ userId: ObjectId("..."), status: "pending" })
  .sort({ createdAt: -1 })
  .explain("executionStats")

// Key fields in output:
// executionStats.executionStages.stage → "IXSCAN" or "COLLSCAN"
// executionStats.totalDocsExamined vs nReturned → should be close

Choosing the right index type

Different index types match different query shapes. Picking the wrong one wastes disk and RAM without speeding up your app.

Real-life example: Index types are different catalog systems — alphabetical by author, by ISBN, or full-text search in the back of the library.

  • Single-field — one key path
  • Compound — multiple fields in one B-tree
  • Multikey — automatic when indexing array fields
  • Text — word search on string fields
  • Unique, TTL, partial — constraints and special behaviour

Single-field and compound indexes

Single-field indexes support equality and range on one path — email, createdAt, status.

Compound indexes cover queries on prefixes of the key list — { a: 1, b: 1, c: 1 } helps { a }, { a, b }, and { a, b, c } but not { b } alone.

ESR rule for compound indexes: Equality fields first, Sort fields second, Range fields last.

Real-life example: Compound index { city: 1, grade: 1, name: 1 } is a phone book sorted by city, then grade, then name — you must know the city to use the grade ordering efficiently.

ESR compound index for a typical query
// Query: city = X, grade in [10,11], name >= "K"
db.students.find({
  "address.city": "Delhi",
  grade: { $in: [10, 11] },
  name: { $gte: "K" }
}).sort({ name: 1 })

// Index following ESR: Equality (city, grade via in), Sort (name), Range on name
db.students.createIndex({
  "address.city": 1,
  grade: 1,
  name: 1
})

Multikey indexes

When you index an array field, MongoDB builds a multikey index — one index entry per array element.

A compound index can include at most one array field. Queries on tags or categories often hit multikey indexes automatically.

Real-life example: Indexing subjects on student documents creates one index entry per subject — finding all Math students uses the index.

Multikey on tags
db.products.createIndex({ tags: 1 })

// Uses multikey index
db.products.find({ tags: "wireless" })

// $all on multiple tags — index still helps first tag
db.products.find({ tags: { $all: ["wireless", "sale"] } })

Text indexes

Text indexes tokenize words for $text search — one text index per collection (can include multiple string fields with weights).

For advanced fuzzy search and autocomplete, Atlas Search is the modern choice; text indexes remain useful for simple search boxes.

Real-life example: Text index on title and body is a magazine index — search 'MongoDB aggregation' and get relevant articles ranked by score.

Text index and $text query
db.articles.createIndex(
  { title: "text", body: "text" },
  { weights: { title: 10, body: 1 }, name: "article_text" }
)

db.articles.find(
  { $text: { $search: "aggregation pipeline tutorial" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

Unique, TTL, and partial indexes (brief)

Unique indexes enforce distinct values — emails, SKUs, slugs. Duplicate inserts fail with E11000.

TTL indexes delete documents when a date field ages out — sessions, OTP codes, log rows.

Partial indexes only index documents matching a filter — smaller index when only active users need speed.

Real-life example: TTL on session.expiresAt is automatic cleanup — like a library that returns overdue books to the shelf without a librarian.

TTL deletion is approximate (background thread ~60s). Do not use TTL as a real-time security boundary for sensitive tokens.
Unique, TTL, and partial
// Unique slug for Rishtaara blog posts
db.posts.createIndex({ slug: 1 }, { unique: true })

// Sessions expire after 24 hours
db.sessions.createIndex(
  { lastActivity: 1 },
  { expireAfterSeconds: 86400 }
)

// Index only active products — saves space
db.products.createIndex(
  { category: 1, price: 1 },
  { partialFilterExpression: { active: true } }
)

Map-Reduce — historical context

Map-Reduce was MongoDB's original batch processing model: map functions emit key-value pairs, reduce functions merge values per key, shuffle sorts in between.

Today the aggregation framework replaces Map-Reduce for almost all workloads — it is faster, easier to debug, and uses the same query optimizer.

Real-life example: Map-Reduce is an old factory line still on the floor — aggregation is the modern line next to it that everyone uses for new orders.

  • Map-Reduce — JavaScript map/reduce functions, slower, harder to maintain
  • Aggregation — declarative stages, index-aware, preferred for new code
  • When to still read Map-Reduce — maintaining legacy jobs, interviews, old tutorials
On Rishtaara and in new production code: default to aggregate(). Only reach for mapReduce() when maintaining existing scripts.

When aggregation replaces Map-Reduce

Word counts, sums by category, log parsing, and sessionization all map cleanly to $match, $group, $project, and $out.

Aggregation supports accumulators, $lookup joins, and pipeline updates — features Map-Reduce never matched ergonomically.

Real-life example: Counting votes per candidate — aggregation is one $group stage; Map-Reduce is two JavaScript functions and a finalize step.

Same report: Map-Reduce vs aggregation
// Modern way — aggregation (preferred)
db.orders.aggregate([
  { $match: { status: "completed" } },
  { $group: { _id: "$category", total: { $sum: "$amount" } } },
  { $sort: { total: -1 } }
])

// Legacy Map-Reduce (awareness only)
db.orders.mapReduce(
  function map() {
    emit(this.category, this.amount);
  },
  function reduce(key, values) {
    return Array.sum(values);
  },
  {
    query: { status: "completed" },
    out: { inline: 1 }
  }
)

Brief mapReduce example — awareness only

mapReduce takes a map function (emit key-value per document), a reduce function (combine values per key), and options like query filter and output collection.

Output can be inline (small results), replace a collection, or merge into existing data.

Real-life example: If you inherit a nightly job that mapReduces web logs into page_view_counts, know what it does — then plan a migration to aggregation + $out.

Inline Map-Reduce — page view counts
db.page_views.mapReduce(
  function () {
    emit(this.page, 1);
  },
  function (key, values) {
    return Array.sum(values);
  },
  {
    query: { ts: { $gte: ISODate("2025-07-01") } },
    out: { inline: 1 }
  }
)

// Equivalent aggregation migration target:
db.page_views.aggregate([
  { $match: { ts: { $gte: ISODate("2025-07-01") } } },
  { $group: { _id: "$page", views: { $sum: 1 } } },
  { $sort: { views: -1 } }
])

Aggregation tools in your toolkit

mongosh — prototype pipelines interactively before embedding in Node.js.

MongoDB Compass — visual aggregation builder and explain plans.

Atlas — Data Explorer, Performance Advisor, and scheduled $out jobs for analytics.

Real-life example: Your Rishtaara learning path — practice in mongosh, ship in Mongoose aggregate(), monitor with explain() in Compass.

  • Use $match + $group for 95% of analytics that old Map-Reduce handled
  • Materialize heavy reports with $out or $merge on a schedule
  • Index early $match fields; use allowDiskUse for large batch jobs
  • Next on Rishtaara: scale topics — replica sets, sharding, and production security
Interview tip: If asked Map-Reduce vs aggregation, say aggregation is preferred — then sketch a $match → $group pipeline for the same problem.

What ACID means in MongoDB

ACID stands for Atomicity, Consistency, Isolation, and Durability — the guarantees relational databases are famous for. MongoDB provides these at different levels depending on what you are updating.

Atomicity means a group of changes either all succeed or all fail — no half-finished state. Consistency means rules (like stock never going negative) stay true. Isolation keeps concurrent transactions from stepping on each other. Durability means committed data survives crashes.

Real-life example: Transferring money between two bank accounts is ACID. Debit one account and credit the other must happen together — never debit without credit, even if the power fails mid-transfer.

  • Single-document updates are always atomic — MongoDB guarantees this on every write.
  • Multi-document ACID transactions require a replica set (even a single-node replica set for local dev).
  • Transactions add overhead — use only when business logic truly needs all-or-nothing across documents.
  • Design with embedding first; reach for transactions when you cannot embed and must keep data consistent.

Single-document atomicity

Every insertOne, updateOne, findOneAndUpdate, or replaceOne on a single document is atomic. MongoDB applies all update operators in one operation — you never get a document with $set applied but $inc skipped.

Embedded arrays and sub-documents inside one document update atomically too. That is why order line items, cart items, and comment threads (when small) belong inside the parent document.

Real-life example: Updating a shopping cart document — adding an item and recalculating total — is one atomic write. The cart never shows a new item with the old total.

Tip: If your data fits in one document, you get atomicity for free — no transaction session needed.
Atomic single-document update
// All operators apply atomically on one document
db.carts.updateOne(
  { userId: ObjectId("665a1b2c3d4e5f6789012345") },
  {
    $push: { items: { productId: ObjectId("..."), qty: 1, price: 899 } },
    $inc: { itemCount: 1, subtotal: 899 },
    $set: { updatedAt: new Date() }
  }
)

Multi-document ACID transactions

Since MongoDB 4.0, you can run transactions across multiple documents and collections in a replica set. Start a session, call withTransaction(), pass { session } to every operation, and commit or abort as a unit.

Typical use cases: deduct inventory and create an order, transfer credits between users, update a parent record and an audit log together. Keep transactions short — long-running transactions block other writers and hurt performance.

Real-life example: A movie ticket booking system reserves a seat and charges payment in one transaction. If payment fails, the seat reservation rolls back automatically.

Multi-document transaction with the native driver
const session = client.startSession();

try {
  await session.withTransaction(async () => {
    const inventory = db.collection("inventory");
    const orders = db.collection("orders");

    const result = await inventory.updateOne(
      { sku: "MOUSE-01", stock: { $gte: 2 } },
      { $inc: { stock: -2 }, $set: { updatedAt: new Date() } },
      { session }
    );

    if (result.modifiedCount !== 1) {
      throw new Error("Insufficient stock");
    }

    await orders.insertOne(
      {
        sku: "MOUSE-01",
        qty: 2,
        total: 1798,
        status: "confirmed",
        createdAt: new Date(),
      },
      { session }
    );
  });
} finally {
  await session.endSession();
}
Transaction options — read concern & write concern
await session.withTransaction(
  async () => { /* operations */ },
  {
    readConcern: { level: "snapshot" },
    writeConcern: { w: "majority" },
    maxCommitTimeMS: 5000,
  }
);

Transactions in Mongoose — session.withTransaction

Mongoose wraps the driver session API. Call mongoose.startSession(), then session.withTransaction() with async callback. Pass { session } to create(), updateOne(), save(), and deleteOne() calls inside the callback.

Mongoose models participate in transactions the same way as raw collections. Use try/finally to always end the session.

Real-life example: Mongoose transactions are like a checkout lane supervisor — every item scan (database write) stays on hold until payment clears, then everything finalizes at once.

Tip: For local development, run mongod as a single-node replica set (rs.initiate()) — standalone instances cannot run multi-document transactions.
Inventory + order with Mongoose
import mongoose from "mongoose";

const inventorySchema = new mongoose.Schema({
  sku: { type: String, unique: true },
  stock: { type: Number, min: 0 },
});
const orderSchema = new mongoose.Schema({
  sku: String,
  qty: Number,
  total: Number,
  status: String,
}, { timestamps: true });

const Inventory = mongoose.model("Inventory", inventorySchema);
const Order = mongoose.model("Order", orderSchema);

async function placeOrder(sku, qty, unitPrice) {
  const session = await mongoose.startSession();
  try {
    await session.withTransaction(async () => {
      const updated = await Inventory.findOneAndUpdate(
        { sku, stock: { $gte: qty } },
        { $inc: { stock: -qty } },
        { session, new: true }
      );

      if (!updated) throw new Error("Out of stock");

      await Order.create(
        [{ sku, qty, total: qty * unitPrice, status: "confirmed" }],
        { session }
      );
    });
  } finally {
    session.endSession();
  }
}

When NOT to use transactions

  • High-throughput writes where eventual consistency is acceptable (analytics counters, view counts).
  • Operations that can be modeled as a single atomic document update.
  • Cross-shard transactions — supported but expensive; redesign shard keys or embed data instead.
  • Long business workflows — use saga patterns or outbox events for multi-step processes spanning minutes.

Relationships in MongoDB

MongoDB does not have foreign keys or JOIN tables like SQL. Instead you model relationships by embedding documents inside a parent or storing references (ObjectIds) to other collections. The right choice depends on how you read and write data.

Real-life example: A school report card can list subjects and grades on one sheet (embedding) or refer to a separate marks register by student ID (reference). Teachers reading one student's full report prefer the sheet; auditors updating one subject across 500 students prefer the register.

  • One-to-one — embed if always accessed together (user + profile settings); reference if large or shared.
  • One-to-many — embed for small, bounded lists (order line items); reference for unbounded (user's orders).
  • Many-to-many — use array of ObjectIds on one side, or a junction collection for complex cases.
  • Always ask: What does my most common query look like? Design the document shape to answer it in one read.

Embed vs reference — decision guide

Embed when data is one-to-few, read together with the parent, and does not grow without limit. Reference when data is large, updated independently, shared across many parents, or queried on its own.

The hybrid pattern embeds a snapshot (name, thumbnail) and keeps the full record in another collection — great for product catalogs where orders need a price snapshot at purchase time.

Real-life example: A pizza order embeds toppings (few items, never queried alone). The customer profile lives in users collection (referenced by userId) because the same person places many orders.

One-to-many — embedded line items
// orders collection — items embedded (one-to-few, read with order)
{
  _id: ObjectId("..."),
  userId: ObjectId("665a1b2c3d4e5f6789012345"),
  status: "completed",
  items: [
    { productId: ObjectId("..."), name: "Wireless Mouse", qty: 2, price: 899 },
    { productId: ObjectId("..."), name: "USB Hub", qty: 1, price: 1299 }
  ],
  total: 3097,
  createdAt: ISODate("2024-06-15T10:30:00Z")
}
Many-to-many — junction pattern
// students collection
{ _id: ObjectId("s1"), name: "Asha", enrolledCourseIds: [ObjectId("c1"), ObjectId("c2")] }

// courses collection
{ _id: ObjectId("c1"), title: "MongoDB Fundamentals", studentCount: 142 }

// Or separate enrollments collection for rich metadata
{ studentId: ObjectId("s1"), courseId: ObjectId("c1"), enrolledAt: ISODate("..."), grade: "A" }
Embed vs reference patterns

Unbounded arrays — the silent killer

Never embed an array that grows forever. Comments on a viral post, log entries, sensor readings, or notification lists will eventually hit the 16 MB document size limit and slow every read/write on that document.

Move unbounded data to a separate collection with a foreign key (postId, userId). Paginate with find().sort().limit(). Index the foreign key field.

Real-life example: A WhatsApp group chat with millions of messages does not store all messages inside the group document — each message is its own record linked by groupId.

Tip: If you ask 'could this array have 10,000+ entries someday?' — use a separate collection. No exceptions.
Bad vs good — comments on a blog post
// BAD — unbounded array inside post document
{
  _id: ObjectId("post1"),
  title: "MongoDB Tips",
  comments: [ /* grows forever — document bloat */ ]
}

// GOOD — separate comments collection
// posts: { _id, title, commentCount: 4821 }
// comments: { postId, authorId, text, createdAt }
db.comments.createIndex({ postId: 1, createdAt: -1 })
db.comments.find({ postId: ObjectId("post1") }).sort({ createdAt: -1 }).limit(20)

Rishtaara modeling checklist

  • List your top 5 queries — design documents to satisfy them in one find() or a simple aggregation.
  • Identify one-to-few vs one-to-many vs many-to-many for each relationship.
  • Embed snapshots at write time for data that can change later (product price, user display name).
  • Index reference fields (userId, postId) on child collections.
  • Run through growth scenarios — what happens at 1M users, 10M orders, 100M events?

Embedding patterns in practice

Embedding groups related data inside a parent document so one read fetches everything your UI needs. Common patterns: address inside user, line items inside order, config inside tenant, metadata inside file record.

Sub-documents can themselves contain arrays and nested objects. Use consistent field naming across documents in a collection even though MongoDB is schema-less — your application code expects predictable shapes.

Real-life example: A restaurant menu embeds categories and dishes inside one menu document. The waiter reads one card (one query) to describe today's specials — no running to the kitchen for each dish name.

Nested embedding — menu with categories and items
db.menus.insertOne({
  restaurantId: ObjectId("..."),
  name: "Rishtaara Cafe — Summer Menu",
  categories: [
    {
      name: "Beverages",
      items: [
        { name: "Masala Chai", price: 49, tags: ["hot", "vegetarian"] },
        { name: "Cold Coffee", price: 99, tags: ["cold"] }
      ]
    },
    {
      name: "Snacks",
      items: [
        { name: "Samosa", price: 30, tags: ["fried", "vegetarian"] }
      ]
    }
  ],
  updatedAt: new Date()
})
Update embedded array element with positional operator
db.menus.updateOne(
  { "categories.items.name": "Masala Chai" },
  { $set: { "categories.$[cat].items.$[item].price": 59 } },
  { arrayFilters: [{ "cat.name": "Beverages" }, { "item.name": "Masala Chai" }] }
)

JSON Schema validation with $jsonSchema

MongoDB lets you attach a JSON Schema validator to a collection. Inserts and updates that violate the schema are rejected. This gives you SQL-like guardrails while keeping document flexibility.

Use validationLevel: "moderate" to validate updates and new inserts but not existing invalid documents. Use validationAction: "error" (default) to reject bad writes.

Real-life example: JSON Schema validation is a bouncer at a club door — checks ID format and dress code before anyone enters, but existing members inside are not kicked out retroactively (with moderate level).

Create collection with $jsonSchema validator
db.createCollection("students", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "email", "grade"],
      properties: {
        name: {
          bsonType: "string",
          description: "Full name — required string"
        },
        email: {
          bsonType: "string",
          pattern: "^[\\w.-]+@[\\w.-]+\\.\\w{2,}$",
          description: "Valid email address"
        },
        grade: {
          bsonType: "int",
          minimum: 1,
          maximum: 12,
          description: "Grade level 1–12"
        },
        subjects: {
          bsonType: "array",
          items: { bsonType: "string" },
          maxItems: 20
        },
        gpa: {
          bsonType: ["double", "decimal", "null"],
          minimum: 0,
          maximum: 10
        }
      },
      additionalProperties: true
    }
  },
  validationLevel: "strict",
  validationAction: "error"
})
Add validator to existing collection
db.runCommand({
  collMod: "students",
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "grade"],
      properties: {
        name: { bsonType: "string" },
        grade: { bsonType: "int", minimum: 1, maximum: 12 }
      }
    }
  },
  validationLevel: "moderate"
})

Common bsonType values and rules

  • bsonType: "object" — embedded document; "array" — list with optional items schema.
  • bsonType: "string", "int", "long", "double", "decimal", "bool", "date", "objectId", "binData".
  • required — array of field names that must exist on every valid document.
  • minimum / maximum — numeric bounds; minLength / maxLength — string and array size limits.
  • enum — restrict to allowed values: { enum: ["active", "inactive", "pending"] }.
  • pattern — regex for string format validation (emails, phone numbers, slugs).
Tip: Combine Mongoose schema validation in your app with $jsonSchema at the database layer for defense in depth — especially in multi-service architectures.
Enum and nested object validation
db.createCollection("orders", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["userId", "status", "items", "total"],
      properties: {
        status: { enum: ["pending", "confirmed", "shipped", "cancelled"] },
        total: { bsonType: ["int", "long", "double"], minimum: 0 },
        items: {
          bsonType: "array",
          minItems: 1,
          items: {
            bsonType: "object",
            required: ["name", "qty", "price"],
            properties: {
              name: { bsonType: "string" },
              qty: { bsonType: "int", minimum: 1 },
              price: { bsonType: "double", minimum: 0 }
            }
          }
        }
      }
    }
  }
})

Why replication matters

Replication copies your data to multiple MongoDB servers so a single hardware failure does not take down your app. A replica set is a group of mongod instances that maintain the same data set — one primary and one or more secondaries.

Replication also enables zero-downtime maintenance (rolling upgrades), disaster recovery, and read scaling by directing read traffic to secondaries.

Real-life example: A replica set is like keeping photocopies of important files in two office cabinets. If one cabinet floods, you still have the other copy — and colleagues can read from either cabinet.

Replica set — primary, secondaries, oplog

Replica set members — primary, secondary, arbiter

  • Primary — receives all write operations. Only one primary exists at a time.
  • Secondary — replicates the primary's oplog (operation log). Can serve reads if read preference allows.
  • Arbiter — participates in elections but holds no data. Lightweight tie-breaker for even-numbered clusters (avoid in production if possible — use an odd count of data-bearing nodes instead).
  • Hidden / delayed secondaries — for analytics snapshots or point-in-time recovery; never receive application reads.
Initiate a single-node replica set (local dev)
# Start mongod with --replSet rs0
mongosh --eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "localhost:27017" }] })'

# Verify status
mongosh --eval 'rs.status()'
Three-member replica set config sketch
rs.initiate({
  _id: "rs0",
  members: [
    { _id: 0, host: "mongo1.example.com:27017", priority: 2 },
    { _id: 1, host: "mongo2.example.com:27017", priority: 1 },
    { _id: 2, host: "mongo3.example.com:27017", priority: 1 }
  ]
})

Deployment basics

Production replica sets use at least three data-bearing nodes across availability zones — or two nodes plus an arbiter only as a budget compromise. MongoDB Atlas handles replica set deployment, backups, and monitoring automatically.

Set electionTimeoutMillis and priority to control failover behavior. Use keyFile authentication so only cluster members can join the set.

Real-life example: Deploying three nodes in different data centers is like having branch offices in three cities — a flood in one city does not stop the whole company.

  • Odd number of voting members prevents split-brain during network partitions.
  • Write concern "majority" waits until a write is replicated to a majority before acknowledging.
  • Read concern "majority" returns data that will not be rolled back after a failover.
  • Atlas M10+ clusters include automated backups with point-in-time restore.

Read/write semantics and read preference

By default, drivers send writes to the primary and reads to the primary too. You can change read preference to distribute read load — but secondary reads may be slightly stale (eventual consistency).

Real-life example: Read preference "secondary" is reading yesterday's newspaper from the archive room — fine for analytics, wrong for breaking news (financial balances).

  • primary (default) — always read from primary; strongest consistency.
  • primaryPreferred — primary if available, else secondary.
  • secondary — only secondaries; fails if none available.
  • secondaryPreferred — secondary if available, else primary.
  • nearest — lowest-latency node; good for geo-distributed apps.
Connection string with read preference
// Analytics reads from secondaries; writes still go to primary
const uri =
  "mongodb://user:pass@host1,host2,host3/rishtaara?replicaSet=rs0&readPreference=secondaryPreferred";

await mongoose.connect(uri);

// Per-query override
const stats = await Order.find({ status: "completed" })
  .read("secondary")
  .limit(1000);

Change streams — brief intro

Change streams let your application watch a collection, database, or entire cluster for insert, update, replace, and delete events in real time. They require a replica set or sharded cluster.

Use cases: invalidate caches, sync to Elasticsearch, trigger notifications, build event-driven microservices.

Real-life example: Change streams are a news ticker — every time data changes in MongoDB, your app gets a push notification instead of polling every few seconds.

Tip: Change streams resume from a token — store resumeAfter on disconnect so you never miss events during restarts.
Watch a collection for changes
const changeStream = db.orders.watch([
  { $match: { "operationType": { $in: ["insert", "update"] } } }
]);

changeStream.on("change", (event) => {
  console.log("Order changed:", event.documentKey._id, event.operationType);
  // Invalidate cache, send webhook, update search index...
});

When to shard

Sharding splits data across multiple servers (shards) so no single machine holds the entire data set. Use sharding when vertical scaling (bigger RAM, faster disk) is no longer enough — typically collections exceeding hundreds of GB or write throughput beyond one replica set.

Most apps never need sharding. A well-indexed replica set on modern hardware handles terabytes for many workloads. Shard when metrics prove you have outgrown a single replica set.

Real-life example: Sharding is opening multiple checkout counters at a supermarket — each counter (shard) handles a portion of customers so no single line blocks everyone.

Sharded cluster components

Sharded cluster components

  • mongos — query router. Your application connects to mongos, not individual shards. mongos routes queries to the correct shard(s).
  • Config servers — store cluster metadata: which chunk lives on which shard, shard key ranges, balancer state. Deploy as a replica set (CSRS).
  • Shards — each shard is a replica set holding a subset of data. Minimum production setup: 3 config servers + 3 shards × 3 nodes each.
  • Chunks — contiguous ranges of shard key values. Balancer migrates chunks between shards to keep data evenly distributed.

Shard keys — the most important decision

The shard key determines how data is distributed. Choose a field (or compound fields) with high cardinality and even distribution. A bad shard key causes hot shards — one server gets all writes while others sit idle.

Real-life example: Sharding by customerId spreads orders across shards evenly. Sharding by orderDate alone sends all today's orders to one shard — a traffic jam on one lane.

Enable sharding and shard a collection
sh.enableSharding("rishtaara")

// Hashed shard key — good even distribution for ObjectId-like fields
sh.shardCollection("rishtaara.events", { userId: "hashed" })

// Compound ranged shard key — tenantId + createdAt for multi-tenant SaaS
sh.shardCollection("rishtaara.logs", { tenantId: 1, createdAt: 1 })

Hashed vs ranged sharding

  • Hashed sharding — MongoDB hashes the shard key value. Even distribution even if keys are sequential (timestamps, auto-increments). Queries must include the full shard key or mongos broadcasts to all shards (scatter-gather).
  • Ranged sharding — data partitioned by key ranges. Efficient range queries (date ranges, alphabetical) if the query includes the shard key prefix. Risk of hot spots with monotonically increasing keys.
  • Compound shard keys — combine a high-cardinality prefix (tenantId, userId) with a range suffix (createdAt) for SaaS and time-series patterns.
  • Immutable shard keys — you cannot change a document's shard key value after insert (MongoDB 5.0+ allows with certain restrictions — still avoid changing shard keys in design).
Targeted vs scatter-gather queries
// TARGETED — includes shard key userId; hits one shard
db.events.find({ userId: ObjectId("665a..."), type: "click" })

// SCATTER-GATHER — no shard key; mongos queries ALL shards
db.events.find({ type: "click", createdAt: { $gte: ISODate("2024-06-01") } })

// Always design queries to include the shard key when possible

Deployment architecture notes

Run at least one mongos per application tier or use a load balancer in front of multiple mongos instances. Config servers need low-latency connectivity to all shards.

Atlas Global Clusters automate geo-sharding — data for Indian users lives in Mumbai, EU users in Frankfurt, with zone-aware shard keys.

Real-life example: Production sharded clusters are airports with multiple terminals (shards), a central flight board (config servers), and information desks (mongos) that direct passengers to the right gate.

  • Test shard key choice with simulated load before committing — resharding is possible but disruptive.
  • Avoid sharding collections smaller than 100 GB — overhead exceeds benefit.
  • Monitor chunk migration and balancer activity — uneven chunk counts signal a bad key.
  • Use zone sharding to pin data to specific regions for data residency (GDPR, India DPDP).
Tip: On Rishtaara-scale apps, a replica set plus good indexes beats premature sharding. Measure first, shard when data proves you must.

Defense in depth for MongoDB

MongoDB security is layered — network isolation, encryption, authentication, authorization, and auditing. Skipping any layer exposes your data. Never run a production database with no authentication on a public IP.

Real-life example: Securing MongoDB is like securing a bank — fence (firewall), locked doors (auth), ID badges (RBAC), cameras (audit logs), and armored trucks (TLS encryption).

MongoDB security layers

Enable access control and authentication

Start mongod with --auth or enable authorization in the config file. Create an admin user first, then application-specific users with least privilege.

SCRAM-SHA-256 is the default authentication mechanism. x.509 certificates work for internal cluster member auth.

Real-life example: Creating separate database users is giving each employee a key card that opens only their floor — the intern cannot enter the vault.

Create admin and application users
// Connect without auth on first setup, create admin
use admin
db.createUser({
  user: "adminUser",
  pwd: "Very-Strong-Random-Password-123!",
  roles: [{ role: "userAdminAnyDatabase", db: "admin" }]
})

// Application user — readWrite only on rishtaara database
use rishtaara
db.createUser({
  user: "rishtaara_app",
  pwd: process.env.MONGO_APP_PASSWORD,
  roles: [{ role: "readWrite", db: "rishtaara" }]
})

// Read-only user for reporting dashboards
db.createUser({
  user: "rishtaara_readonly",
  pwd: process.env.MONGO_READONLY_PASSWORD,
  roles: [{ role: "read", db: "rishtaara" }]
})

Role-based access control (RBAC)

  • Built-in roles: read, readWrite, dbAdmin, userAdmin, clusterAdmin, root.
  • Custom roles combine specific privileges: { resource: { db: "rishtaara", collection: "orders" }, actions: ["find", "insert"] }.
  • Principle of least privilege — your API server gets readWrite on its database, not root.
  • Atlas IAM integration and LDAP/SAML for enterprise single sign-on.
Custom role — insert-only on logs collection
use rishtaara
db.createRole({
  role: "logWriter",
  privileges: [
    {
      resource: { db: "rishtaara", collection: "logs" },
      actions: ["insert"]
    }
  ],
  roles: []
})

db.createUser({
  user: "log_service",
  pwd: "service-password",
  roles: [{ role: "logWriter", db: "rishtaara" }]
})

TLS, connection strings, and environment variables

Always use TLS in production. Atlas enforces TLS by default. Self-hosted clusters need certificates configured on mongod and in the connection string (?tls=true).

Store credentials in environment variables — never commit connection strings to Git. Use .env locally and secret managers (AWS Secrets Manager, Vault) in production.

Real-life example: Putting passwords in source code is writing your house key under the doormat — anyone who clones the repo gets in.

Secure connection string patterns
# .env — never commit this file
MONGODB_URI=mongodb+srv://rishtaara_app:<PASSWORD>@cluster0.xxxxx.mongodb.net/rishtaara?retryWrites=true&w=majority

# Node.js
await mongoose.connect(process.env.MONGODB_URI);

NoSQL injection — validate all input

NoSQL injection happens when user input becomes part of a query object without sanitization. An attacker sends { "email": { "$gt": "" } } instead of a string and bypasses login checks.

Real-life example: NoSQL injection is tricking the receptionist with a fake ID format — 'anyone whose name is greater than empty string' matches every record.

Tip: Use mongoose schema types, celebrate/joi validation, and never pass req.body directly into find(), findOne(), or updateOne() filters.
Vulnerable vs safe login query
// VULNERABLE — user body merged into query
app.post("/login", async (req, res) => {
  const user = await User.findOne(req.body); // attacker sends { email: { $gt: "" } }
});

// SAFE — explicit typed fields only
app.post("/login", async (req, res) => {
  const email = String(req.body.email ?? "").trim().toLowerCase();
  const password = String(req.body.password ?? "");
  if (!email || !password) return res.status(400).json({ error: "Missing fields" });

  const user = await User.findOne({ email }).select("+passwordHash");
  // verify password with bcrypt...
});

Why Mongoose for Node.js apps

Mongoose is the most popular ODM (Object Document Mapper) for MongoDB and Node.js. It adds schemas, validation, middleware hooks, population (like joins), and a fluent query API on top of the official MongoDB driver.

Real-life example: The raw MongoDB driver is a manual car — full control, more work. Mongoose is an automatic with cruise control — sensible defaults, guardrails, and less boilerplate for everyday CRUD.

  • Schema — defines shape, types, defaults, and validation rules.
  • Model — constructor compiled from schema; maps to a collection.
  • Document — instance of a model representing one MongoDB document.
  • Middleware — pre/post hooks on save, validate, remove, and aggregate.

Connect MongoDB with Mongoose

Project setup and connection
// npm install mongoose
import mongoose from "mongoose";

const MONGODB_URI = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017/rishtaara";

export async function connectDB() {
  if (mongoose.connection.readyState >= 1) return;

  await mongoose.connect(MONGODB_URI, {
    serverSelectionTimeoutMS: 5000,
  });

  console.log("MongoDB connected:", mongoose.connection.name);
}

// Graceful shutdown
process.on("SIGINT", async () => {
  await mongoose.disconnect();
  process.exit(0);
});

Schema, model, and CRUD

Define schema and model
const courseSchema = new mongoose.Schema(
  {
    title: { type: String, required: true, trim: true },
    slug: { type: String, required: true, unique: true, lowercase: true },
    level: { type: String, enum: ["beginner", "intermediate", "advanced"] },
    lessons: { type: Number, default: 0 },
    tags: [String],
    instructor: {
      name: String,
      bio: String,
    },
    published: { type: Boolean, default: false },
  },
  { timestamps: true }
);

courseSchema.index({ slug: 1 });
courseSchema.index({ published: 1, level: 1 });

export const Course = mongoose.model("Course", courseSchema);
CRUD operations
// Create
const course = await Course.create({
  title: "MongoDB Fundamentals",
  slug: "mongodb-fundamentals",
  level: "beginner",
  tags: ["database", "nosql"],
  instructor: { name: "Rishtaara Team" },
});

// Read — with filter, projection, sort, pagination
const published = await Course.find({ published: true, level: "beginner" })
  .select("title slug lessons")
  .sort({ createdAt: -1 })
  .skip(0)
  .limit(10);

const one = await Course.findOne({ slug: "mongodb-fundamentals" });

// Update
await Course.updateOne(
  { slug: "mongodb-fundamentals" },
  { $inc: { lessons: 1 }, $set: { published: true } }
);

// Delete
await Course.deleteOne({ slug: "draft-course" });

.lean() and aggregate tips

.lean() tells Mongoose to return plain JavaScript objects instead of full Mongoose documents. Skips change tracking and hydration — significantly faster for read-only API responses.

Use Model.aggregate() for analytics pipelines. Mongoose aggregate returns plain objects by default (no .lean() needed).

Real-life example: .lean() is photocopying a report instead of borrowing the original binder — faster to hand out, but you cannot edit the master copy through the photocopy.

Tip: Add .explain() to Mongoose queries in development: Course.find({ published: true }).explain('executionStats') — same as mongosh explain.
lean() for API performance
// Read-only list endpoint — use lean()
app.get("/api/courses", async (req, res) => {
  const courses = await Course.find({ published: true })
    .select("title slug level lessons")
    .sort({ createdAt: -1 })
    .limit(20)
    .lean();

  res.json(courses);
});

// When you need save() and middleware — do NOT use lean()
const doc = await Course.findOne({ slug: "mongodb-fundamentals" });
doc.lessons += 1;
await doc.save();
Aggregation with Mongoose
const stats = await Course.aggregate([
  { $match: { published: true } },
  {
    $group: {
      _id: "$level",
      count: { $sum: 1 },
      avgLessons: { $avg: "$lessons" },
    },
  },
  { $sort: { count: -1 } },
]);

// Populate — reference join (use sparingly on hot paths)
const userSchema = new mongoose.Schema({
  name: String,
  courseIds: [{ type: mongoose.Schema.Types.ObjectId, ref: "Course" }],
});
const users = await User.find().populate("courseIds", "title slug");

Project overview — auth with Express + Mongoose

Build a minimal signup and login API for a Rishtaara-style learning platform. Stack: Express.js, Mongoose, bcrypt for password hashing, and JSON Web Tokens or sessions for keeping users logged in.

Real-life example: Signup is registering for a library card. Login is showing that card at the desk. bcrypt hashes the secret PIN so even the librarian cannot read your password.

  • Never store plain-text passwords — always hash with bcrypt (cost factor 10–12).
  • Validate email format and password length in the route before touching the database.
  • Return generic error messages on login failure — do not reveal whether email exists.
  • Use HTTP-only cookies for session tokens in browser apps; Bearer tokens for mobile/API clients.

User schema and signup route

User model with password hash
import mongoose from "mongoose";
import bcrypt from "bcrypt";

const SALT_ROUNDS = 12;

const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true, trim: true },
    email: {
      type: String,
      required: true,
      unique: true,
      lowercase: true,
      trim: true,
    },
    passwordHash: { type: String, required: true, select: false },
    role: { type: String, enum: ["student", "instructor"], default: "student" },
  },
  { timestamps: true }
);

userSchema.pre("save", async function hashPassword(next) {
  if (!this.isModified("passwordHash")) return next();
  // passwordHash field receives plain password temporarily in signup flow
  this.passwordHash = await bcrypt.hash(this.passwordHash, SALT_ROUNDS);
  next();
});

userSchema.methods.comparePassword = function (plain) {
  return bcrypt.compare(plain, this.passwordHash);
};

export const User = mongoose.model("User", userSchema);
POST /api/signup — Express route
import express from "express";
import { User } from "./models/User.js";

const router = express.Router();

router.post("/signup", async (req, res) => {
  try {
    const name = String(req.body.name ?? "").trim();
    const email = String(req.body.email ?? "").trim().toLowerCase();
    const password = String(req.body.password ?? "");

    if (!name || !email || password.length < 8) {
      return res.status(400).json({ error: "Invalid input" });
    }

    const existing = await User.findOne({ email });
    if (existing) {
      return res.status(409).json({ error: "Email already registered" });
    }

    const user = await User.create({
      name,
      email,
      passwordHash: password, // pre-save hook hashes this
    });

    res.status(201).json({
      id: user._id,
      name: user.name,
      email: user.email,
    });
  } catch (err) {
    res.status(500).json({ error: "Signup failed" });
  }
});

export default router;

Login route with bcrypt verification

POST /api/login
router.post("/login", async (req, res) => {
  try {
    const email = String(req.body.email ?? "").trim().toLowerCase();
    const password = String(req.body.password ?? "");

    if (!email || !password) {
      return res.status(400).json({ error: "Email and password required" });
    }

    const user = await User.findOne({ email }).select("+passwordHash");
    if (!user) {
      return res.status(401).json({ error: "Invalid email or password" });
    }

    const match = await user.comparePassword(password);
    if (!match) {
      return res.status(401).json({ error: "Invalid email or password" });
    }

    // Issue session token (JWT sketch)
    const token = createToken({ userId: user._id, role: user.role });

    res.json({
      token,
      user: { id: user._id, name: user.name, email: user.email, role: user.role },
    });
  } catch (err) {
    res.status(500).json({ error: "Login failed" });
  }
});
Auth middleware — protect routes
export function requireAuth(req, res, next) {
  const header = req.headers.authorization ?? "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;

  if (!token) return res.status(401).json({ error: "Unauthorized" });

  try {
    req.user = verifyToken(token);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

router.get("/profile", requireAuth, async (req, res) => {
  const user = await User.findById(req.user.userId).select("name email role");
  res.json(user);
});

Simple HTML form sketch (optional frontend)

Tip: Extend this project on Rishtaara — add course enrollment, progress tracking, and protect /api/enroll with requireAuth. Store only hashed passwords; never log req.body.password.
Signup and login forms
<!-- Signup -->
<form action="/api/signup" method="POST">
  <input name="name" placeholder="Full name" required />
  <input name="email" type="email" placeholder="Email" required />
  <input name="password" type="password" minlength="8" required />
  <button type="submit">Create account</button>
</form>

<!-- Login -->
<form action="/api/login" method="POST">
  <input name="email" type="email" placeholder="Email" required />
  <input name="password" type="password" required />
  <button type="submit">Log in</button>
</form>

Django and MongoDB — the landscape

Django's ORM is built for relational databases (PostgreSQL, MySQL). MongoDB is document-oriented, so there is no official Django-MongoDB backend. Python developers use bridges like mongoengine or Djongo, or skip the ORM and use pymongo directly.

Real-life example: Connecting Django to MongoDB is like using a universal power adapter abroad — it works, but native plugs (PostgreSQL + Django ORM) fit more naturally.

  • mongoengine — standalone ODM, Django-like models but separate from Django ORM. Most popular for Django + MongoDB projects.
  • Djongo — translates Django ORM queries to MongoDB. Convenient but less maintained; check compatibility with your Django version.
  • pymongo — official driver; use in Django views/services when you want full control without an ODM layer.
  • For new Django projects needing MongoDB, many teams run Django with PostgreSQL for auth/admin and a separate Node/Python service for MongoDB-heavy features.

mongoengine connection sketch

settings.py — connect with mongoengine
# pip install mongoengine django
import os
import mongoengine

MONGODB_URI = os.environ.get(
    "MONGODB_URI",
    "mongodb://127.0.0.1:27017/rishtaara"
)

def connect_mongodb():
    mongoengine.connect(
        host=MONGODB_URI,
        alias="default",
    )

# Call in AppConfig.ready() or wsgi.py startup
connect_mongodb()
Document model and CRUD
from mongoengine import (
    Document, StringField, IntField, ListField,
    EmailField, DateTimeField,
)
from datetime import datetime

class Student(Document):
    meta = {"collection": "students"}
    name = StringField(required=True, max_length=120)
    email = EmailField(required=True, unique=True)
    grade = IntField(min_value=1, max_value=12)
    subjects = ListField(StringField())
    created_at = DateTimeField(default=datetime.utcnow)

# Create
student = Student(name="Asha", email="asha@school.edu", grade=10, subjects=["Math"])
student.save()

# Read
students = Student.objects(grade__gte=10).order_by("name")[:20]
one = Student.objects(email="asha@school.edu").first()

# Update
Student.objects(email="asha@school.edu").update(add_to_set__subjects="Physics")

# Delete
Student.objects(email="asha@school.edu").delete()

When Django + MongoDB makes sense vs PostgreSQL

Choose Django + MongoDB when you already have MongoDB data (IoT events, content documents, activity feeds) and want Python admin tooling or REST APIs alongside it. mongoengine models work well for document-shaped domains.

Choose Django + PostgreSQL when you need Django Admin on relational data, complex joins, foreign keys, and mature migration tooling (django migrations). Most Django tutorials and packages assume SQL.

Real-life example: A CMS with flexible article JSON bodies fits MongoDB. An e-commerce checkout with inventory locks and refunds fits PostgreSQL (or MongoDB with careful transaction design).

  • Django Admin does not work out-of-the-box with mongoengine documents — build custom admin or use PostgreSQL for admin-managed entities.
  • Use PostgreSQL for users, billing, and permissions; MongoDB for logs, analytics events, and user-generated content if you need both.
  • Atlas Data API or a thin FastAPI/Express layer often beats forcing Django ORM patterns onto MongoDB.
Tip: On Rishtaara, the MongoDB course focuses on Node.js + Mongoose for app integration. Use this lesson as a map for Python teams — mongoengine docs are the next stop.

Hands-on exercises

Practice cements theory. Complete these exercises on a local replica set or Atlas free tier. Save your mongosh scripts and Mongoose code in a GitHub repo for portfolio proof.

Real-life example: Exercises are gym reps for your database skills — interviews test whether you can lift real queries under pressure, not just read the manual.

  • Design schemas for a food delivery app: restaurants, menus (embedded), orders (embedded line items + userId reference), drivers. Justify embed vs reference for each.
  • Implement inventory deduction + order creation as a Mongoose transaction. Simulate failure (out of stock) and verify rollback.
  • Add $jsonSchema validation to an orders collection — required fields, status enum, minimum one line item.
  • Set up a 3-node replica set locally (or Atlas). Test readPreference secondaryPreferred and observe stale reads.
  • Build signup/login API with Express, Mongoose, bcrypt. Protect a /api/my-courses route with JWT middleware.
  • Write aggregation pipelines: revenue by month, top 10 products by quantity sold, active users in last 7 days.
  • Create indexes for your heaviest queries; run explain() before and after and document the improvement.
  • Enable auth on your cluster. Create readWrite and read-only users. Connect via environment variable only.

Interview question themes

MongoDB interviews focus on practical design and query skills, not trivia. Expect live coding in mongosh or a driver, schema design whiteboards, and trade-off discussions.

  • CRUD — write find/update/delete queries with operators ($gt, $in, $elemMatch, $regex). Explain findOneAndUpdate vs updateOne.
  • Indexing — ESR rule, compound indexes, unique indexes, partial indexes, TTL indexes. Interpret explain() output (IXSCAN vs COLLSCAN).
  • Aggregation — build pipelines with $match, $group, $lookup, $unwind, $project. When to use aggregation vs find.
  • Data modeling — embed vs reference, unbounded arrays, hybrid patterns, anti-patterns (massive documents, too many indexes).
  • Transactions — when to use, single-document atomicity vs multi-document, performance cost.
  • Replication & sharding — replica set failover, read preferences, write concern, shard key selection, hot shards.
  • Security — RBAC, NoSQL injection prevention, TLS, connection string secrets.
  • Mongoose — schema design, middleware, lean(), populate pitfalls, session.withTransaction().
Tip: Always explain your trade-offs aloud in interviews — 'I embed line items because orders are read as a unit and item count is bounded' beats memorizing definitions.

Continue learning on Rishtaara

  • MCQ Quiz — test fundamentals quickly: /mcq/mongodb-mcq
  • Interview prep — common questions with model answers: /interview/mongodb-interview
  • Knowledge hub — full MongoDB reference: /knowledge/mongodb-fundamentals
  • SQL course — compare relational vs document thinking: /learn/data-science/sql-zero-to-analyst

Online quiz and next courses

Take the Rishtaara MongoDB MCQ quiz until you score 85%+ twice in a row — that usually means the core concepts have stuck. Then attempt the interview question set under timed conditions.

From here, explore adjacent Rishtaara paths: Node.js backend patterns, Next.js full-stack with MongoDB API routes, or data engineering with aggregation-heavy pipelines.

Real-life example: Finishing this course is earning your motorcycle license — you can ride solo now, but highway traffic (production sharding, multi-region failover) comes with more miles.

Congratulations on completing the Rishtaara MongoDB course — from documents and CRUD through scaling, security, and real app projects. Ship something, break it, fix it, and keep querying.

Key Takeaways

  • MongoDB stores flexible BSON documents in collections — design schemas around how you read data.
  • CRUD: insertOne/Many, find with filters, update with $ operators, deleteOne/Many.
  • Query and expression operators ($gt, $in, $and, $elemMatch, $inc, $push) build precise filters and updates.
  • Indexes turn collection scans into index scans — always explain() production queries.
  • Aggregation pipelines ($match, $group, $lookup) power analytics without exporting data.
  • Embed one-to-few data; reference shared or large data; use transactions when multi-document consistency matters.
  • Replica sets give high availability; sharding scales writes horizontally with a good shard key.
  • Secure MongoDB with auth, RBAC, TLS, and sanitized query inputs — never expose an open 27017.

Frequently Asked Questions

When should I use MongoDB instead of MySQL or PostgreSQL?
Choose MongoDB when your data is document-shaped, schemas evolve often, and you rarely need complex multi-table joins. Use MySQL/PostgreSQL when relational integrity and SQL reporting are central.
What is the difference between find() and aggregate()?
find() returns matching documents mostly as stored. aggregate() runs a pipeline of stages — grouping, joining, computing fields — and returns reshaped results.
BSON aur JSON mein kya farak hai?
JSON is text. BSON is MongoDB’s binary format — it supports extra types (ObjectId, Date, Binary) and is faster for the database to scan and store.
How do I avoid NoSQL injection?
Never pass raw user objects into query filters. Validate types, whitelist allowed fields, and keep connection strings in environment variables.
Does MongoDB support JOINs?
$lookup in aggregation performs a left outer join. For hot paths, prefer embedding or denormalization over frequent $lookup.
Replica set aur sharding mein kya farak hai?
Replication copies the same data for availability and failover. Sharding splits data across machines so one cluster can hold more data and handle more writes.