Curated questions covering CRUD operations, aggregation pipeline, indexing, replication, sharding, transactions, and Mongoose ODM.
MongoDB is a NoSQL document database that stores data as BSON (Binary JSON) documents. Differences from RDBMS: no fixed schema (flexible documents), no JOINs (embed or reference), horizontal scaling via sharding, no ACID transactions by default (added in v4.0), and collections instead of tables.
BSON (Binary JSON) is the binary-encoded serialization format MongoDB uses to store documents. It extends JSON with additional types: Date, ObjectId, Binary, Decimal128, and more. BSON is more efficient to parse than JSON and supports richer data types.
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"name": "Alice",
"age": 30,
"createdAt": ISODate("2024-01-01"),
"scores": [95, 87, 92],
"address": { "city": "NYC", "zip": "10001" }
}
// Embedding
{ name: "Alice", address: { city: "NYC", zip: "10001" } }
// Referencing
{ name: "Alice", addressId: ObjectId("...") }
db.users.insertOne({ name: "Alice", age: 30 });
db.users.find({ age: { $gt: 25 } }, { name: 1, _id: 0 });
db.users.updateOne({ name: "Alice" }, { $set: { age: 31 } });
db.users.deleteOne({ name: "Alice" });
db.users.findOne({ email: "alice@example.com" }); // single doc or null
db.users.find({ age: { $gte: 18 } }).limit(10); // cursor
db.products.find({
price: { $gte: 10, $lte: 100 },
category: { $in: ["electronics", "books"] },
tags: { $all: ["sale", "new"] }
});
The aggregation pipeline processes documents through a sequence of stages, each transforming the data. Common stages: $match, $group, $project, $sort, $limit, $skip, $lookup, $unwind, $addFields, $count.
db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 10 }
]);
$lookup performs a left outer join between two collections, similar to SQL JOIN. It adds an array field to each document with matching documents from the joined collection.
db.orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" }
]);
$unwind deconstructs an array field, outputting one document per array element. Used before $group or $lookup when working with array fields.
// Document: { name: "Alice", scores: [90, 85, 92] }
db.students.aggregate([
{ $unwind: "$scores" }
]);
// Outputs 3 documents:
// { name: "Alice", scores: 90 }
// { name: "Alice", scores: 85 }
// { name: "Alice", scores: 92 }
db.sales.aggregate([{
$group: {
_id: "$category",
count: { $sum: 1 },
avgPrice: { $avg: "$price" },
allTags: { $push: "$tag" },
uniqueTags: { $addToSet: "$tag" }
}
}]);
Indexes improve query performance by allowing MongoDB to find documents without scanning the entire collection. Without an index, MongoDB performs a collection scan (COLLSCAN). With an index, it performs an index scan (IXSCAN).
db.users.createIndex({ email: 1 }); // ascending
db.users.createIndex({ age: -1 }); // descending
db.users.createIndex({ email: 1 }, { unique: true }); // unique
db.users.getIndexes(); // list indexes
db.users.find({ email: "a@b.com" }).explain("executionStats");
db.users.createIndex({ lastName: 1, firstName: 1 }); // compound
db.products.createIndex({ tags: 1 }); // multikey (tags is array)
db.articles.createIndex({ content: "text" }); // text index
db.users.updateOne({ _id: id }, {
$set: { age: 31, "address.city": "LA" },
$unset: { tempField: "" },
$inc: { loginCount: 1 },
$push: { tags: "premium" }
});
// updateOne - only changes age
db.users.updateOne({ _id: id }, { $set: { age: 31 } });
// replaceOne - entire document replaced
db.users.replaceOne({ _id: id }, { name: "Alice", age: 31 });
MongoDB 4.0+ supports multi-document ACID transactions. Use sessions to group operations. Transactions work across multiple documents and collections.
const session = client.startSession();
try {
session.startTransaction();
await db.accounts.updateOne(
{ _id: fromId }, { $inc: { balance: -100 } }, { session }
);
await db.accounts.updateOne(
{ _id: toId }, { $inc: { balance: 100 } }, { session }
);
await session.commitTransaction();
} catch (e) {
await session.abortTransaction();
} finally {
session.endSession();
}
A replica set is a group of MongoDB instances that maintain the same dataset. It provides redundancy and high availability. The primary receives all writes; secondaries replicate from the primary. If the primary fails, an election promotes a secondary.
// $project - only name and computed fullName
{ $project: { name: 1, fullName: { $concat: ["$first", " ", "$last"] } } }
// $addFields - adds fullName, keeps all other fields
{ $addFields: { fullName: { $concat: ["$first", " ", "$last"] } } }
$facet runs multiple sub-pipelines on the same input documents simultaneously, returning results in a single document. Useful for faceted search (multiple categorizations at once).
db.products.aggregate([{
$facet: {
byCategory: [{ $group: { _id: "$category", count: { $sum: 1 } } }],
byPrice: [{ $bucket: { groupBy: "$price", boundaries: [0,50,100,500] } }],
total: [{ $count: "count" }]
}
}]);
// Simple count
db.users.aggregate([{ $match: { active: true } }, { $count: "activeUsers" }]);
// Count with other stats
db.users.aggregate([{ $group: { _id: null, count: { $sum: 1 }, avgAge: { $avg: "$age" } } }]);
// ObjectId
const id = new ObjectId();
console.log(id.getTimestamp()); // creation time
// Custom _id
db.users.insertOne({ _id: "custom-id-123", name: "Alice" });
// Prefer $in for same field
db.users.find({ status: { $in: ["active", "pending"] } });
// Use $or for different fields
db.users.find({ $or: [{ email: "a@b.com" }, { phone: "123" }] });
// Sparse index
db.users.createIndex({ phone: 1 }, { sparse: true });
// Partial index
db.users.createIndex({ email: 1 }, {
partialFilterExpression: { status: "active" }
});
// Regex (slow for non-prefix)
db.articles.find({ title: { $regex: /mongodb/i } });
// Text search (fast with text index)
db.articles.createIndex({ title: "text", body: "text" });
db.articles.find({ $text: { $search: "mongodb aggregation" } });
Mongoose is an ODM (Object Document Mapper) for MongoDB in Node.js. It adds: schema definition with validation, middleware (pre/post hooks), virtuals, population (reference resolution), query building, and TypeScript support.
const userSchema = new Schema({
email: { type: String, required: true, unique: true },
age: { type: Number, min: 0, max: 150 }
});
userSchema.pre("save", async function() {
this.password = await bcrypt.hash(this.password, 10);
});
const User = model("User", userSchema);
// Mongoose populate (multiple queries)
const order = await Order.findById(id).populate("customer");
// $lookup (single query)
db.orders.aggregate([{ $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }]);
// $bucket - manual boundaries
{ $bucket: { groupBy: "$price", boundaries: [0, 25, 50, 100, 500], default: "Other" } }
// $bucketAuto - automatic
{ $bucketAuto: { groupBy: "$price", buckets: 4 } }
// $out - replaces collection
{ $out: "monthly_summary" }
// $merge - upsert into existing
{ $merge: { into: "monthly_summary", on: "_id", whenMatched: "merge", whenNotMatched: "insert" } }
// Returns update result, not the document
await db.users.updateOne({ _id: id }, { $set: { age: 31 } });
// Returns the updated document
const updated = await db.users.findOneAndUpdate(
{ _id: id },
{ $set: { age: 31 } },
{ returnDocument: "after" }
);
db.users.updateOne({ _id: id }, { $push: { tags: "premium" } }); // allows duplicates
db.users.updateOne({ _id: id }, { $addToSet: { tags: "premium" } }); // unique only
db.users.updateOne({ _id: id }, { $pull: { tags: "expired" } }); // remove by value
db.users.updateOne({ _id: id }, { $pop: { scores: 1 } }); // remove last
db.users.updateOne({ _id: id }, { $pop: { scores: -1 } }); // remove first
await db.users.countDocuments({ active: true }); // accurate, with filter
await db.users.estimatedDocumentCount(); // fast estimate, no filter
$expr allows using aggregation expressions within a query filter. It enables comparing fields within the same document, which regular operators cannot do.
// Compare two fields in the same document
db.orders.find({
$expr: { $gt: ["$totalAmount", "$discountAmount"] }
});
// Cannot do this with regular operators
// db.orders.find({ totalAmount: { $gt: "$discountAmount" } }) // WRONG
const changeStream = db.collection("orders").watch();
changeStream.on("change", (change) => {
console.log("Change detected:", change.operationType);
});
// Find all ancestors of an employee
db.employees.aggregate([{
$graphLookup: {
from: "employees",
startWith: "$managerId",
connectFromField: "managerId",
connectToField: "_id",
as: "reportingHierarchy"
}
}]);
db.sales.aggregate([{
$setWindowFields: {
partitionBy: "$category",
sortBy: { date: 1 },
output: {
runningTotal: { $sum: "$amount", window: { documents: ["unbounded", "current"] } }
}
}
}]);
Time series collections (MongoDB 5.0+) are optimized for storing time-stamped data (IoT, metrics, logs). They automatically compress data, improve query performance for time-range queries, and support automatic expiration via TTL.
db.createCollection("sensorData", {
timeseries: {
timeField: "timestamp",
metaField: "sensorId",
granularity: "seconds"
},
expireAfterSeconds: 86400 // auto-delete after 1 day
});
const result = await users.updateOne({ email }, { $set: { active: true } });
const user = await users.findOneAndUpdate(
{ email },
{ $set: { active: true } },
{ returnDocument: "after" }
);
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ phone: 1 }, { unique: true, sparse: true });
A covered query can be answered entirely from an index because the filter fields and returned fields are all included in the index. It avoids reading full documents, which can reduce I/O and improve performance.
db.users.createIndex({ email: 1, status: 1 });
// Covered if _id is excluded and only indexed fields are returned
db.users.find(
{ email: "a@b.com" },
{ _id: 0, email: 1, status: 1 }
);
Explore 500+ free tutorials across 20+ languages and frameworks.