All topics
Database · Learning hub

MongoDB notes for developers

Master MongoDB with a curated set of 3 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Database notes
MongoDB

Interview Questions

MongoDB Interview Questions Q: When would you choose MongoDB over PostgreSQL? Choose MongoDB when: documents naturally belong together and don't need to be join

MongoDB Interview Questions

Q: When would you choose MongoDB over PostgreSQL?

Choose MongoDB when: documents naturally belong together and don't need to be joined (e.g., a blog post with comments, a product with variants), schema needs to evolve rapidly, you store hierarchical or polymorphic data. Choose PostgreSQL when: data is highly relational, you need strong ACID guarantees across many entities, complex queries with JOINs are common, or you need advanced analytics.

Q: What is embedding vs referencing in MongoDB?

Embedding stores related data inside the same document (fast reads, single query, document limit 16MB). Referencing stores an _id and requires a second query or $lookup (like a JOIN). Embed when data is always accessed together and has a 1:few relationship. Reference when data is large, frequently updated independently, or has many-to-many relationships.

Q: What is a replica set?

A group of MongoDB servers that maintain the same dataset. One primary accepts writes; secondaries replicate asynchronously. If the primary fails, an election promotes a secondary. Provides high availability and read scaling (read from secondaries). Atlas manages replica sets automatically.

Q: What is sharding?

Horizontal scaling by splitting data across multiple shards (servers) based on a shard key. Each shard holds a subset of the data. A mongos router directs queries to the right shard. Choose the shard key carefully — high cardinality, even distribution, commonly used in queries. Poor shard key choice leads to hotspots.

Q: Does MongoDB support transactions?

Yes, since v4.0 — multi-document ACID transactions across collections and databases on replica sets, and across shards since v4.2. However, transactions are more expensive than in relational DBs. The MongoDB data model encourages designing documents to avoid needing cross-document transactions.

Q: What is the aggregation pipeline?

A framework for data processing and transformation. Documents flow through an ordered series of stages ($match, $group, $sort, $lookup, $project, $unwind, etc.), each transforming the data. It's more powerful than find() for analytics, reporting, and data reshaping. Stages are processed server-side and can use indexes ($match and $sort at the start).

Q: What is the ESR rule for compound indexes?

Equality → Sort → Range. Put fields used in equality comparisons first, then fields used for sorting, then fields used in range queries. This makes the index as effective as possible because equal-value fields narrow the dataset most, sort fields allow in-order traversal, and range fields are at the end.

MongoDB

CRUD & Queries

CRUD & Queries Core Concepts Document — JSON-like record (BSON internally) Collection — group of documents (like a table, but schema-less) _id — auto-created Ob

CRUD & Queries

Core Concepts

  • Document — JSON-like record (BSON internally)

  • Collection — group of documents (like a table, but schema-less)

  • _id — auto-created ObjectId, globally unique, immutable

  • Database — namespace of collections

Insert

// mongosh / Node.js (mongodb driver)
const db = client.db('mydb');
const users = db.collection('users');

// Insert one
const result = await users.insertOne({
  name: 'Alice',
  email: 'alice@example.com',
  age: 28,
  roles: ['user'],
  address: { city: 'NYC', country: 'US' },
  createdAt: new Date(),
});
console.log(result.insertedId);  // ObjectId

// Insert many
await users.insertMany([
  { name: 'Bob', email: 'bob@example.com' },
  { name: 'Carol', email: 'carol@example.com' },
], { ordered: false });  // continue on error if false

Find & Query

// Find all
const allUsers = await users.find({}).toArray();

// Find with filter
const adults = await users.find({ age: { $gte: 18 } }).toArray();

// Find one
const user = await users.findOne({ email: 'alice@example.com' });

// Find by _id
const { ObjectId } = require('mongodb');
const user = await users.findOne({ _id: new ObjectId('64abc123...') });

// Projection (include/exclude fields)
const names = await users.find({}, { projection: { name: 1, email: 1, _id: 0 } }).toArray();

// Sort, skip, limit
const paginated = await users.find({ active: true })
  .sort({ createdAt: -1 })
  .skip(20)
  .limit(10)
  .toArray();

// Count
const count = await users.countDocuments({ age: { $gte: 18 } });

// Comparison operators
// $eq  $ne  $gt  $gte  $lt  $lte  $in  $nin
await users.find({ age: { $gte: 18, $lte: 65 } }).toArray();
await users.find({ role: { $in: ['admin', 'mod'] } }).toArray();
await users.find({ status: { $nin: ['banned', 'deleted'] } }).toArray();

// Logical operators
await users.find({
  $and: [{ age: { $gte: 18 } }, { active: true }]
}).toArray();

await users.find({
  $or: [{ email: 'a@b.com' }, { name: 'Admin' }]
}).toArray();

await users.find({ premium: { $not: { $eq: true } } }).toArray();

// Element operators
await users.find({ phone: { $exists: true } }).toArray();
await users.find({ age: { $type: 'number' } }).toArray();

// Array operators
await users.find({ roles: 'admin' }).toArray();          // contains 'admin'
await users.find({ tags: { $all: ['js', 'ts'] } }).toArray(); // contains all
await users.find({ tags: { $size: 3 } }).toArray();

// Regex
await users.find({ name: /^alice/i }).toArray();
await users.find({ name: { $regex: '^alice', $options: 'i' } }).toArray();

// Nested field
await users.find({ 'address.city': 'NYC' }).toArray();

Update

// Update one
await users.updateOne(
  { _id: userId },
  {
    $set: { name: 'Alice B.', updatedAt: new Date() },
    $inc: { loginCount: 1 },     // increment
    $push: { tags: 'premium' },  // add to array
    $pull: { tags: 'free' },     // remove from array
    $unset: { tempField: '' },   // remove field
  }
);

// Update many
await users.updateMany(
  { active: false, lastLoginAt: { $lt: new Date('2023-01-01') } },
  { $set: { status: 'inactive' } }
);

// Upsert
await users.updateOne(
  { email: 'newuser@example.com' },
  { $setOnInsert: { createdAt: new Date() }, $set: { name: 'New User' } },
  { upsert: true }
);

// findOneAndUpdate — returns the document
const updated = await users.findOneAndUpdate(
  { _id: userId },
  { $set: { lastSeen: new Date() } },
  { returnDocument: 'after' }   // return updated doc
);

// Array update operators
await users.updateOne({ _id: userId }, {
  $addToSet: { tags: 'new-tag' },          // add if not exists (like a set)
  $pop: { items: -1 },                      // remove first (-1) or last (1)
  $pullAll: { tags: ['old', 'deprecated'] } // remove multiple values
});

Delete

// Delete one
await users.deleteOne({ _id: userId });

// Delete many
await users.deleteMany({ active: false, createdAt: { $lt: cutoffDate } });

// Find and delete — returns the deleted doc
const deleted = await users.findOneAndDelete({ _id: userId });
MongoDB

Aggregation & Indexes

Aggregation & Indexes Aggregation Pipeline Stages are processed in order, each stage's output is the next stage's input. const pipeline = [ // $match — filter (

Aggregation & Indexes

Aggregation Pipeline

Stages are processed in order, each stage's output is the next stage's input.

const pipeline = [
  // $match — filter (like WHERE, use early to limit docs)
  { $match: { active: true, age: { $gte: 18 } } },

  // $group — group and aggregate (like GROUP BY)
  {
    $group: {
      _id: '$country',
      count: { $sum: 1 },
      avgAge: { $avg: '$age' },
      emails: { $push: '$email' },
      minAge: { $min: '$age' },
      maxAge: { $max: '$age' },
    }
  },

  // $sort — sort results
  { $sort: { count: -1 } },

  // $limit and $skip — pagination
  { $skip: 0 },
  { $limit: 10 },

  // $project — reshape documents
  {
    $project: {
      country: '$_id',
      count: 1,
      avgAge: { $round: ['$avgAge', 1] },
      _id: 0,
    }
  },
];

const results = await users.aggregate(pipeline).toArray();

// $lookup — JOIN another collection
const ordersWithUsers = await orders.aggregate([
  {
    $lookup: {
      from: 'users',
      localField: 'userId',
      foreignField: '_id',
      as: 'user',
    }
  },
  { $unwind: '$user' },            // flatten array to single doc
  { $project: { total: 1, 'user.name': 1, 'user.email': 1 } },
]).toArray();

// $unwind — flatten array field
await posts.aggregate([
  { $unwind: '$tags' },            // one doc per tag
  { $group: { _id: '$tags', count: { $sum: 1 } } },
  { $sort: { count: -1 } },
]).toArray();

// $addFields / $set
await users.aggregate([
  { $addFields: { fullName: { $concat: ['$firstName', ' ', '$lastName'] } } },
]).toArray();

// $facet — multiple pipelines in one pass
await products.aggregate([
  {
    $facet: {
      totalCount: [{ $count: 'count' }],
      byCategory: [{ $group: { _id: '$category', count: { $sum: 1 } } }],
      priceStats: [{ $group: { _id: null, avg: { $avg: '$price' }, min: { $min: '$price' } } }],
    }
  }
]).toArray();

Indexes

// Single field
await users.createIndex({ email: 1 });           // ascending
await users.createIndex({ createdAt: -1 });      // descending (for latest first)

// Unique index
await users.createIndex({ email: 1 }, { unique: true });

// Compound index (order matters — use ESR rule: Equality, Sort, Range)
await users.createIndex({ country: 1, createdAt: -1 });

// Partial index — index only matching documents
await users.createIndex(
  { email: 1 },
  { partialFilterExpression: { active: true } }
);

// Sparse index — skip documents where field doesn't exist
await users.createIndex({ phone: 1 }, { sparse: true });

// TTL index — auto-delete documents after N seconds
await sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 86400 }); // 24h

// Text index for full-text search
await articles.createIndex({ title: 'text', body: 'text' });
await articles.find({ $text: { $search: 'mongodb performance' } }).toArray();

// Wildcard index
await products.createIndex({ 'attributes.$**': 1 });

// List indexes
await users.listIndexes().toArray();

// Drop index
await users.dropIndex({ email: 1 });

// Explain
await users.find({ email: 'a@b.com' }).explain('executionStats');

Mongoose (ODM)

import mongoose, { Schema, model, Document } from 'mongoose';

// Schema definition
const userSchema = new Schema({
  email: { type: String, required: true, unique: true, lowercase: true, trim: true },
  name: { type: String, required: true, minlength: 2 },
  age: { type: Number, min: 0, max: 150 },
  roles: [{ type: String, enum: ['user', 'admin', 'mod'] }],
  address: {
    city: String,
    country: { type: String, default: 'US' },
  },
}, {
  timestamps: true,            // adds createdAt, updatedAt
  toJSON: { virtuals: true },
});

// Virtual field
userSchema.virtual('isAdult').get(function() {
  return this.age >= 18;
});

// Methods
userSchema.methods.greet = function() {
  return `Hello, ${this.name}`;
};

// Statics
userSchema.statics.findByEmail = function(email: string) {
  return this.findOne({ email: email.toLowerCase() });
};

// Pre-save hook
userSchema.pre('save', async function(next) {
  if (this.isModified('password')) {
    this.password = await bcrypt.hash(this.password, 10);
  }
  next();
});

const User = model('User', userSchema);

// CRUD with Mongoose
const user = await User.create({ email: 'a@b.com', name: 'Alice' });
const users = await User.find({ active: true }).select('name email').lean();
await User.findByIdAndUpdate(id, { $inc: { loginCount: 1 } }, { new: true });
await User.findByIdAndDelete(id);

Keep your MongoDB knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever