MongoDB
MongoDB is a document-oriented NoSQL database that stores data in flexible, JSON-like documents called BSON (Binary JSON). Unlike relational databases, MongoDB documents can have varying structures, making it ideal for rapidly evolving schemas and hierarchical data.
MongoDB's document model maps naturally to objects in application code, eliminating the object-relational impedance mismatch that plagues traditional ORM usage. Its aggregation framework provides powerful data transformation capabilities that rival SQL's analytical queries.
This guide covers CRUD operations, the aggregation pipeline, indexing strategies, multi-document transactions, and schema design patterns for production applications.
note
In MongoDB, data is organized into databases, collections (analogous to tables), and documents (analogous to rows). Documents are stored in BSON format, which supports all JSON types plus additional types like Date, ObjectId, Binary, and Decimal128.
| 1 | // A MongoDB document structure |
| 2 | { |
| 3 | _id: ObjectId("665f1a2b3c4d5e6f7a8b9c0d"), |
| 4 | name: "Alice Johnson", |
| 5 | email: "alice@example.com", |
| 6 | age: 32, |
| 7 | isActive: true, |
| 8 | createdAt: ISODate("2025-01-15T10:30:00Z"), |
| 9 | |
| 10 | // Embedded document (1:1 or 1:few) |
| 11 | address: { |
| 12 | street: "123 Main St", |
| 13 | city: "San Francisco", |
| 14 | state: "CA", |
| 15 | zip: "94102", |
| 16 | coordinates: { type: "Point", coordinates: [-122.4194, 37.7749] } |
| 17 | }, |
| 18 | |
| 19 | // Array of subdocuments (1:many, bounded) |
| 20 | preferences: [ |
| 21 | { key: "theme", value: "dark" }, |
| 22 | { key: "language", value: "en" }, |
| 23 | { key: "notifications", value: "email" } |
| 24 | ], |
| 25 | |
| 26 | // Array of references (1:many, unbounded) |
| 27 | orderIds: [ |
| 28 | ObjectId("665f1b3c4d5e6f7a8b9c0e1f"), |
| 29 | ObjectId("665f1c4d5e6f7a8b9c0e2f3a") |
| 30 | ], |
| 31 | |
| 32 | // Schema version for migrations |
| 33 | _schemaVersion: 2 |
| 34 | } |
| 35 | |
| 36 | // Collections are schema-flexible โ different documents |
| 37 | // in the same collection can have different fields |
| 38 | db.users.insertOne({ |
| 39 | name: "Bob Smith", |
| 40 | email: "bob@example.com", |
| 41 | // No address, no preferences โ different shape, same collection |
| 42 | }); |
info
MongoDB provides a rich set of CRUD operations with dot-notation access for nested fields and array manipulation operators.
| 1 | // CREATE operations |
| 2 | db.users.insertOne({ |
| 3 | name: "Charlie Brown", |
| 4 | email: "charlie@example.com", |
| 5 | role: "developer", |
| 6 | skills: ["javascript", "python", "go"], |
| 7 | createdAt: new Date() |
| 8 | }); |
| 9 | |
| 10 | db.users.insertMany([ |
| 11 | { name: "Diana Ross", email: "diana@example.com", role: "designer" }, |
| 12 | { name: "Eve Wilson", email: "eve@example.com", role: "developer" }, |
| 13 | { name: "Frank Lee", email: "frank@example.com", role: "manager" } |
| 14 | ]); |
| 15 | |
| 16 | // READ operations |
| 17 | db.users.findOne({ email: "charlie@example.com" }); |
| 18 | |
| 19 | db.users.find({ |
| 20 | role: "developer", |
| 21 | skills: { $in: ["javascript", "typescript"] }, |
| 22 | createdAt: { $gte: ISODate("2025-01-01") } |
| 23 | }).sort({ createdAt: -1 }).limit(10).projection({ |
| 24 | name: 1, email: 1, skills: 1 |
| 25 | }); |
| 26 | |
| 27 | // UPDATE operations |
| 28 | db.users.updateOne( |
| 29 | { _id: ObjectId("...") }, |
| 30 | { |
| 31 | $set: { role: "senior developer", updatedAt: new Date() }, |
| 32 | $push: { skills: { $each: ["rust", "postgresql"] } }, |
| 33 | $inc: { version: 1 } |
| 34 | } |
| 35 | ); |
| 36 | |
| 37 | // Array updates |
| 38 | db.users.updateOne( |
| 39 | { _id: ObjectId("...") }, |
| 40 | { $pull: { skills: "python" } } // remove from array |
| 41 | ); |
| 42 | |
| 43 | db.users.updateOne( |
| 44 | { _id: ObjectId("...") }, |
| 45 | { $addToSet: { skills: "rust" } } // add if not exists |
| 46 | ); |
| 47 | |
| 48 | // DELETE operations |
| 49 | db.users.deleteOne({ _id: ObjectId("...") }); |
| 50 | db.users.deleteMany({ role: { $exists: false } }); |
The aggregation pipeline is MongoDB's most powerful feature. It processes documents through a sequence of stages, each transforming the data. Think of it as SQL's SELECT pipeline broken into composable, readable stages.
| 1 | // E-commerce analytics pipeline |
| 2 | db.orders.aggregate([ |
| 3 | // Stage 1: Filter (like WHERE) |
| 4 | { $match: { |
| 5 | status: "completed", |
| 6 | createdAt: { $gte: ISODate("2025-01-01") } |
| 7 | }}, |
| 8 | |
| 9 | // Stage 2: Unwind array (like JOIN LATERAL) |
| 10 | { $unwind: "$items" }, |
| 11 | |
| 12 | // Stage 3: Lookup (like LEFT JOIN) |
| 13 | { $lookup: { |
| 14 | from: "products", |
| 15 | localField: "items.productId", |
| 16 | foreignField: "_id", |
| 17 | as: "product" |
| 18 | }}, |
| 19 | |
| 20 | // Stage 4: Deconstruct array to single field |
| 21 | { $unwind: "$product" }, |
| 22 | |
| 23 | // Stage 5: Compute new fields |
| 24 | { $addFields: { |
| 25 | itemRevenue: { $multiply: ["$items.quantity", "$items.unitPrice"] }, |
| 26 | productCategory: "$product.category", |
| 27 | margin: { |
| 28 | $subtract: [ |
| 29 | "$items.unitPrice", |
| 30 | { $multiply: ["$product.cost", 0.7] } |
| 31 | ] |
| 32 | } |
| 33 | }}, |
| 34 | |
| 35 | // Stage 6: Group (like GROUP BY) |
| 36 | { $group: { |
| 37 | _id: { |
| 38 | month: { $dateToString: { format: "%Y-%m", date: "$createdAt" } }, |
| 39 | category: "$productCategory" |
| 40 | }, |
| 41 | totalRevenue: { $sum: "$itemRevenue" }, |
| 42 | totalOrders: { $sum: 1 }, |
| 43 | avgOrderValue: { $avg: "$itemRevenue" }, |
| 44 | totalMargin: { $sum: "$margin" }, |
| 45 | uniqueCustomers: { $addToSet: "$userId" } |
| 46 | }}, |
| 47 | |
| 48 | // Stage 7: Reshape output |
| 49 | { $project: { |
| 50 | _id: 0, |
| 51 | month: "$_id.month", |
| 52 | category: "$_id.category", |
| 53 | totalRevenue: { $round: ["$totalRevenue", 2] }, |
| 54 | totalOrders: 1, |
| 55 | avgOrderValue: { $round: ["$avgOrderValue", 2] }, |
| 56 | totalMargin: { $round: ["$totalMargin", 2] }, |
| 57 | customerCount: { $size: "$uniqueCustomers" } |
| 58 | }}, |
| 59 | |
| 60 | // Stage 8: Sort (like ORDER BY) |
| 61 | { $sort: { month: -1, totalRevenue: -1 } }, |
| 62 | |
| 63 | // Stage 9: Limit |
| 64 | { $limit: 50 } |
| 65 | ]); |
| 1 | // Advanced aggregation: User retention analysis |
| 2 | db.orders.aggregate([ |
| 3 | { $match: { createdAt: { $gte: ISODate("2025-01-01") } } }, |
| 4 | |
| 5 | // Get each user's first order date |
| 6 | { $group: { |
| 7 | _id: "$userId", |
| 8 | firstOrder: { $min: "$createdAt" }, |
| 9 | orders: { $push: { date: "$createdAt", total: "$total" } } |
| 10 | }}, |
| 11 | |
| 12 | // Calculate cohort month |
| 13 | { $addFields: { |
| 14 | cohortMonth: { $dateToString: { format: "%Y-%m", date: "$firstOrder" } }, |
| 15 | orderMonths: { |
| 16 | $map: { |
| 17 | input: "$orders", |
| 18 | as: "o", |
| 19 | in: { $dateToString: { format: "%Y-%m", date: "$$o.date" } } |
| 20 | } |
| 21 | } |
| 22 | }}, |
| 23 | |
| 24 | // Unwind months and count unique users per cohort-month |
| 25 | { $unwind: "$orderMonths" }, |
| 26 | { $group: { |
| 27 | _id: { cohort: "$cohortMonth", activityMonth: "$orderMonths" }, |
| 28 | activeUsers: { $sum: 1 } |
| 29 | }}, |
| 30 | |
| 31 | // Calculate retention rate |
| 32 | { $lookup: { |
| 33 | from: (function() { return null; })(), // self-reference in pipeline |
| 34 | let: { cohort: "$_id.cohort" }, |
| 35 | pipeline: [ |
| 36 | { $match: { $expr: { $eq: ["$cohortMonth", "$$cohort"] } } }, |
| 37 | { $count: "total" } |
| 38 | ], |
| 39 | as: "cohortSize" |
| 40 | }}, |
| 41 | |
| 42 | { $project: { |
| 43 | cohort: "$_id.cohort", |
| 44 | month: "$_id.activityMonth", |
| 45 | activeUsers: 1, |
| 46 | retentionRate: { |
| 47 | $round: [ |
| 48 | { $multiply: [ |
| 49 | { $divide: ["$activeUsers", { $arrayElemAt: ["$cohortSize.total", 0] }] }, |
| 50 | 100 |
| 51 | ]}, |
| 52 | 1 |
| 53 | ] |
| 54 | } |
| 55 | }}, |
| 56 | |
| 57 | { $sort: { cohort: 1, month: 1 } } |
| 58 | ]); |
pro tip
MongoDB supports several index types to optimize different query patterns. Choosing the right index is critical for performance.
| 1 | // Single field index |
| 2 | db.users.createIndex({ email: 1 }, { unique: true }); |
| 3 | |
| 4 | // Compound index (order matters!) |
| 5 | db.orders.createIndex({ userId: 1, createdAt: -1 }); |
| 6 | db.orders.createIndex({ status: 1, createdAt: -1, total: -1 }); |
| 7 | |
| 8 | // Multikey index (automatically indexes array elements) |
| 9 | db.users.createIndex({ skills: 1 }); |
| 10 | |
| 11 | // Text index (full-text search) |
| 12 | db.articles.createIndex({ title: "text", body: "text" }, { |
| 13 | weights: { title: 10, body: 1 }, |
| 14 | default_language: "english" |
| 15 | }); |
| 16 | |
| 17 | // Geospatial index |
| 18 | db.locations.createIndex({ "address.coordinates": "2dsphere" }); |
| 19 | |
| 20 | // TTL index (auto-delete documents after time) |
| 21 | db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); |
| 22 | |
| 23 | // Partial index (index only a subset) |
| 24 | db.orders.createIndex( |
| 25 | { userId: 1, createdAt: -1 }, |
| 26 | { partialFilterExpression: { status: "completed" } } |
| 27 | ); |
| 28 | |
| 29 | // Sparse index (skip documents without the field) |
| 30 | db.users.createIndex({ phone: 1 }, { sparse: true }); |
| 31 | |
| 32 | // Hashed index (for hash-based sharding) |
| 33 | db.users.createIndex({ email: "hashed" }); |
| 34 | |
| 35 | // Wildcard index (index all fields matching a pattern) |
| 36 | db.products.createIndex({ "metadata.$**": 1 }); |
| 37 | |
| 38 | // Explain query execution |
| 39 | db.users.find({ email: "test@example.com" }).explain("executionStats"); |
| 40 | |
| 41 | // Check index usage statistics |
| 42 | db.users.aggregate([{ $indexStats: {} }]); |
warning
MongoDB supports multi-document ACID transactions since version 4.0. While single-document operations are always atomic, transactions are needed when you must atomically update multiple documents across collections.
| 1 | const { MongoClient } = require("mongodb"); |
| 2 | const client = new MongoClient(process.env.MONGODB_URI); |
| 3 | |
| 4 | async function transferFunds(fromId, toId, amount) { |
| 5 | const session = client.startSession(); |
| 6 | |
| 7 | try { |
| 8 | const result = await session.withTransaction(async () => { |
| 9 | const users = client.db("myapp").collection("users"); |
| 10 | |
| 11 | // Debit sender |
| 12 | const debitResult = await users.updateOne( |
| 13 | { _id: fromId, balance: { $gte: amount } }, |
| 14 | { $inc: { balance: -amount } }, |
| 15 | { session } |
| 16 | ); |
| 17 | |
| 18 | if (debitResult.modifiedCount === 0) { |
| 19 | throw new Error("Insufficient funds"); |
| 20 | } |
| 21 | |
| 22 | // Credit receiver |
| 23 | await users.updateOne( |
| 24 | { _id: toId }, |
| 25 | { $inc: { balance: amount } }, |
| 26 | { session } |
| 27 | ); |
| 28 | |
| 29 | // Record transaction |
| 30 | await client.db("myapp").collection("transactions").insertOne({ |
| 31 | fromId, toId, amount, |
| 32 | createdAt: new Date(), |
| 33 | type: "transfer" |
| 34 | }, { session }); |
| 35 | |
| 36 | return { success: true }; |
| 37 | }); |
| 38 | |
| 39 | return result; |
| 40 | } finally { |
| 41 | await session.endSession(); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | // Usage |
| 46 | await transferFunds( |
| 47 | ObjectId("from-user-id"), |
| 48 | ObjectId("to-user-id"), |
| 49 | 100.00 |
| 50 | ); |
warning
MongoDB schema design is about making deliberate trade-offs between embedding (denormalization) and referencing (normalization). These patterns guide those decisions.
| 1 | // Pattern 1: Extended Reference Pattern |
| 2 | // Store frequently accessed fields alongside the reference |
| 3 | db.orders.aggregate([ |
| 4 | { $lookup: { |
| 5 | from: "users", |
| 6 | localField: "userId", |
| 7 | foreignField: "_id", |
| 8 | as: "user", |
| 9 | pipeline: [ |
| 10 | { $project: { name: 1, email: 1 } } // only needed fields |
| 11 | ] |
| 12 | }}, |
| 13 | { $unwind: "$user" } |
| 14 | ]); |
| 15 | |
| 16 | // Pattern 2: Subset Pattern |
| 17 | // Embed only recent/frequent data, reference older data |
| 18 | db.users.updateOne( |
| 19 | { _id: userId }, |
| 20 | { |
| 21 | $push: { |
| 22 | recentOrders: { |
| 23 | $each: [newOrder], |
| 24 | $slice: -10 // keep only last 10 |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | ); |
| 29 | |
| 30 | // Pattern 3: Bucket Pattern (time-series) |
| 31 | // Group related time-series data into time-based buckets |
| 32 | db.sensorData.updateOne( |
| 33 | { sensorId: "s1", date: ISODate("2025-01-15"), count: { $lt: 500 } }, |
| 34 | { |
| 35 | $push: { measurements: { ts: new Date(), value: 42.5 } }, |
| 36 | $inc: { count: 1 }, |
| 37 | $setOnInsert: { createdAt: new Date() } |
| 38 | }, |
| 39 | { upsert: true } |
| 40 | ); |
| 41 | |
| 42 | // Pattern 4: Computed Pattern |
| 43 | // Pre-compute expensive aggregations |
| 44 | db.products.updateOne( |
| 45 | { _id: productId }, |
| 46 | { |
| 47 | $set: { |
| 48 | "stats.avgRating": { $avg: "$reviews.rating" }, |
| 49 | "stats.reviewCount": { $size: "$reviews" }, |
| 50 | "stats.lastReviewAt": { $max: "$reviews.createdAt" } |
| 51 | } |
| 52 | } |
| 53 | ); |
| 54 | |
| 55 | // Pattern 5: Document Versioning Pattern |
| 56 | db.articles.updateOne( |
| 57 | { _id: articleId }, |
| 58 | { |
| 59 | $set: { |
| 60 | title: "Updated Title", |
| 61 | content: "Updated content", |
| 62 | _schemaVersion: 3, |
| 63 | updatedAt: new Date() |
| 64 | }, |
| 65 | $push: { |
| 66 | versions: { |
| 67 | $each: [{ title: "Old Title", content: "Old content", savedAt: new Date() }], |
| 68 | $slice: -10 |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | ); |
info
Change streams provide a real-time feed of data changes, similar to PostgreSQL's logical replication. They enable event-driven architectures, real-time updates, and data synchronization between systems.
| 1 | // Watch for changes on a collection |
| 2 | const changeStream = db.orders.watch([ |
| 3 | { $match: { "operationType": { $in: ["insert", "update"] } } } |
| 4 | ]); |
| 5 | |
| 6 | changeStream.on("change", (change) => { |
| 7 | switch (change.operationType) { |
| 8 | case "insert": |
| 9 | console.log("New order:", change.fullDocument); |
| 10 | // Trigger email notification, inventory update, etc. |
| 11 | break; |
| 12 | case "update": |
| 13 | console.log("Order updated:", change.documentKey._id); |
| 14 | console.log("Updated fields:", change.updateDescription.updatedFields); |
| 15 | break; |
| 16 | } |
| 17 | }); |
| 18 | |
| 19 | // Watch specific fields only |
| 20 | const pipeline = [ |
| 21 | { $match: { "fullDocument.status": "completed" } }, |
| 22 | { $project: { fullDocument: 1, operationType: 1 } } |
| 23 | ]; |
| 24 | const stream = db.orders.watch(pipeline, { fullDocument: "updateLookup" }); |
| 25 | |
| 26 | // Resume from a specific point |
| 27 | const resumeToken = ...; // save this for later |
| 28 | const resumedStream = db.orders.watch([], { resumeAfter: resumeToken }); |
| 29 | |
| 30 | // Change streams with the Node.js driver |
| 31 | const collection = client.db("myapp").collection("orders"); |
| 32 | const pipeline = [{ $match: { "operationType": "insert" } }]; |
| 33 | const changeStream = collection.watch(pipeline, { |
| 34 | fullDocument: "updateLookup" |
| 35 | }); |
| 36 | |
| 37 | for await (const event of changeStream) { |
| 38 | console.log("Change event:", event); |
| 39 | // Process event... |
| 40 | } |
MongoDB provides built-in profiling, server statistics, and explain plans to help you identify and fix performance issues.
| 1 | // Enable profiling (log slow queries) |
| 2 | db.setProfilingLevel(1, { slowms: 100 }); // log queries > 100ms |
| 3 | |
| 4 | // Check slow queries |
| 5 | db.system.profile.find({ millis: { $gt: 100 } }) |
| 6 | .sort({ ts: -1 }) |
| 7 | .limit(10) |
| 8 | .pretty(); |
| 9 | |
| 10 | // Server status |
| 11 | db.serverStatus(); // comprehensive server stats |
| 12 | db.serverStatus().connections; // connection info |
| 13 | db.serverStatus().wiredTiger; // storage engine stats |
| 14 | |
| 15 | // Collection stats |
| 16 | db.orders.stats(); |
| 17 | db.orders.stats({ indexDetails: true }); |
| 18 | |
| 19 | // Current operations |
| 20 | db.currentOp({ "secs_running": { $gte: 5 } }); |
| 21 | |
| 22 | // Explain a query |
| 23 | db.orders.find({ userId: "u1", status: "completed" }) |
| 24 | .explain("executionStats"); |
| 25 | |
| 26 | // Key metrics to watch: |
| 27 | // - executionStats.totalDocsExamined (should be close to nReturned) |
| 28 | // - executionStats.totalKeysExamined (should be close to nReturned) |
| 29 | // - executionStats.executionTimeMillis |
| 30 | // - If totalDocsExamined >> nReturned, you need a better index |
| 31 | |
| 32 | // Index suggestion |
| 33 | db.orders.find({ userId: "u1", status: "completed", createdAt: { $gte: new Date() } }) |
| 34 | .explain("executionStats"); |
| 35 | // If it does a collection scan, create: |
| 36 | // db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 }); |
best practice