MongoDB Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for freshers and experienced developers in India preparing for interviews where MongoDB is part of the job: backend, full-stack and data roles at product companies, startups and IT services firms. It has 60 questions with answers in three levels. Easy covers documents, BSON, CRUD and query operators; medium covers indexes, explain, aggregation, data modelling and the bugs teams meet in real projects; hard covers transactions, replica sets, read and write concerns and sharding. Answers follow MongoDB 7 behaviour and name the version where something changed. Answer each question aloud first, try the code in mongosh on a test database, then take the timed mock test.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
- 495 open MongoDB jobs
What MongoDB interviews cover
- Documents and BSON: data types, _id and ObjectId, the 16 MB document limit, and how MongoDB compares with SQL databases.
- CRUD and queries: insert, update and upsert, query and projection operators, and queries on arrays and embedded documents.
- Indexes and performance: single-field, compound, multikey, text and TTL indexes, reading explain output, and pagination.
- Aggregation and data modelling: $match, $group, $lookup and $unwind, embedding versus referencing, and schema validation.
- For experienced roles: replica sets, read and write concerns, transactions, change streams, sharding and shard keys.
How to prepare (freshers and experienced developers)
- Freshers: run MongoDB Community locally or use a free Atlas cluster, create a small collection of your own (students, orders, jobs) and practise find, update and aggregate in mongosh until the operators feel natural.
- Run explain("executionStats") on the queries you write and learn what COLLSCAN, IXSCAN, totalKeysExamined and totalDocsExamined tell you.
- Experienced developers: be ready to design a schema for a real product, such as an e-commerce order system or a chat app, and defend embed versus reference with expected sizes and read patterns.
- Revise the operational topics in the official documentation for your version: write concern defaults, transactions, replica set elections and shard key choice.
- Prepare one story about a MongoDB performance problem you fixed: what explain showed, which index or model change you made, and the result.
Common mistakes and interview-day tips
- Do not call MongoDB schema-less and stop there. Say the schema is flexible, and that you enforce it with validation and in the application.
- Do not copy SQL tables one to one into collections; explain how the way the data is read drives the model.
- Name the version when behaviour changed, for example the majority default write concern from MongoDB 5.0 or multi-document transactions from 4.0.
- When asked about a slow query, start with explain and the indexes, not with more hardware.
- Never promise a result order without a sort: MongoDB does not guarantee one.
MongoDB interview questions: Easy questions (freshers)
20 questions1. What is MongoDB, and how is a document database different from a relational database?
MongoDB is a document database: it stores records as JSON-like documents (BSON) grouped in collections, instead of rows in tables with a fixed set of columns. A document can hold nested objects and arrays, so an order and its line items can live in one record instead of three joined tables. Documents in one collection may have different fields, which makes changes easy, though you can still enforce rules with schema validation. Replica sets give it high availability and sharding lets it scale out. A relational database is often the better fit for highly connected data with many ad hoc joins.
2. How do common SQL terms map to MongoDB, and how would you write a simple SELECT as a MongoDB query?
A table becomes a collection, a row becomes a document, a column becomes a field, and the primary key is the _id field. The WHERE clause becomes the first argument of find (the filter) and the column list becomes the second (the projection). _id is returned unless you exclude it, which is why the query sets _id: 0. Joins map to embedding related data or to the $lookup stage, and GROUP BY maps to $group. A common slip is assuming the mapping is one to one: a good MongoDB design often has fewer collections than the SQL design had tables.
-- SQL
SELECT name, city FROM students WHERE city = 'Pune';
// MongoDB (mongosh)
db.students.find(
{ city: "Pune" },
{ name: 1, city: 1, _id: 0 }
)3. What is BSON, and why does MongoDB store BSON instead of plain JSON text?
BSON is the binary encoding MongoDB uses for documents on disk and over the network. It adds types that JSON lacks, such as Date, ObjectId, 32-bit and 64-bit integers, Decimal128 and binary data, so joinedOn above is a real date that sorts and compares as a date, not as text. Strings and embedded documents carry their length, so the server can skip over fields quickly. One document can be at most 16 MB; larger files go into GridFS. A common mistake is storing dates as strings like 01-07-2024, which then sort and compare as text.
db.employees.insertOne({
empCode: "EMP1042",
name: "Kavya Nair",
joinedOn: new Date("2024-07-01"),
deptId: ObjectId("65f1c2a9e4b0a1b2c3d4e5f6"),
leaveBalance: NumberInt(18),
skills: ["java", "mongodb"],
address: { city: "Kochi", pin: "682001" }
})4. What is the _id field, and what does an ObjectId contain?
Every document needs an _id that is unique within its collection. It acts as the primary key, gets a unique index automatically and cannot be changed after insert. If you do not supply one, the driver adds an ObjectId: 12 bytes made of a 4-byte timestamp in seconds, a 5-byte random value fixed per process, and a 3-byte counter. So getTimestamp() tells you roughly when it was created, and sorting by _id is close to insert order, but not exact across many app servers. You may use your own _id, such as an employee code, if it is unique and never changes.
5. What is the difference between ordered and unordered insertMany when one document fails?
insertMany is ordered by default: it inserts in array order and stops at the first error. Run A inserts Asha and Ravi, then fails on Meena at index 2 with a duplicate key error, and Kiran is never tried. With ordered: false the server tries every document, so run B inserts three (Asha, Ravi and Kiran) and reports the one failure at index 2. In both runs the documents already inserted stay, because insertMany is not a transaction. Use unordered inserts for bulk loads where each record stands alone, and read the error to see which indexes failed.
// employees is empty before each run
const docs = [
{ _id: 1, name: "Asha" },
{ _id: 2, name: "Ravi" },
{ _id: 1, name: "Meena" }, // duplicate _id
{ _id: 3, name: "Kiran" }
];
db.employees.insertMany(docs) // run A
db.employees.insertMany(docs, { ordered: false }) // run B6. How do the comparison operators $gte, $lt, $in and $ne work in a find filter?
Each operator goes inside the field's condition. { marks: { $gte: 60, $lt: 80 } } means 60 or more and below 80, so Query A returns Meena (74) and Kiran (60). $in matches any value in the list, so B returns Asha, Meena, Farhan and Divya; it is the clean way to write several equality checks on one field. $ne means not equal, so C returns Ravi, Kiran and Farhan; note that $ne also matches documents where the field is missing. Without a sort MongoDB promises no result order, so add .sort() when order matters.
// students
{ _id: 1, name: "Asha", city: "Pune", marks: 82 }
{ _id: 2, name: "Ravi", city: "Indore", marks: 58 }
{ _id: 3, name: "Meena", city: "Pune", marks: 74 }
{ _id: 4, name: "Kiran", city: "Jaipur", marks: 60 }
{ _id: 5, name: "Farhan", city: "Surat", marks: 91 }
{ _id: 6, name: "Divya", city: "Pune", marks: 45 }
db.students.find({ marks: { $gte: 60, $lt: 80 } }) // A
db.students.find({ city: { $in: ["Pune", "Surat"] } }) // B
db.students.find({ city: { $ne: "Pune" } }) // C7. How do you combine conditions with AND and OR, and what goes wrong in query C?
Several fields in one filter form an implicit AND, so A returns only Asha (Pune and above 80). $or takes an array of conditions and matches if any is true, so B returns Asha, Meena, Divya and Farhan. Query C is a trap: a JavaScript object literal keeps only the last value of a repeated key, so the second marks silently replaces the first and the filter becomes marks below 70. Divya (45) appears although the intent was 50 to 70. Put both bounds in one object, { marks: { $gt: 50, $lt: 70 } }, or use an explicit $and.
// students
{ _id: 1, name: "Asha", city: "Pune", marks: 82 }
{ _id: 2, name: "Ravi", city: "Indore", marks: 58 }
{ _id: 3, name: "Meena", city: "Pune", marks: 74 }
{ _id: 4, name: "Kiran", city: "Jaipur", marks: 60 }
{ _id: 5, name: "Farhan", city: "Surat", marks: 91 }
{ _id: 6, name: "Divya", city: "Pune", marks: 45 }
db.students.find({ city: "Pune", marks: { $gt: 80 } }) // A
db.students.find({ $or: [ { city: "Pune" },
{ marks: { $gt: 80 } } ] }) // B
db.students.find({ marks: { $gt: 50 }, marks: { $lt: 70 } })
// C8. What is a projection, and what are the rules for including and excluding fields?
A projection is the second argument of find and decides which fields come back: 1 means include and 0 means exclude. One projection may list only inclusions or only exclusions; mixing them, as in the last line, is an error. The exception is _id, which is returned by default and can be turned off with _id: 0 inside an inclusion projection. Projections cut network traffic and memory, and they make covered queries possible. A common mistake is fetching whole documents with large arrays when the page shows only a name and a city.
db.students.find({ city: "Pune" }, { name: 1, marks: 1 })
// name, marks and _id
db.students.find({ city: "Pune" }, { name: 1, _id: 0 })
// name only
db.students.find({ city: "Pune" }, { marks: 0 })
// every field except marks
db.students.find({ city: "Pune" }, { name: 1, marks: 0 })
// error: inclusion and exclusion mixed9. What is the difference between updateOne, updateMany and replaceOne, and why can modifiedCount be lower than matchedCount?
updateOne changes one matching document and updateMany changes all of them; both need update operators such as $set, $inc or $unset, and a plain document without operators is rejected. replaceOne swaps the whole document except _id, so Kiran loses his city field. The updateMany above reports matchedCount 3 but modifiedCount 1, because Asha and Meena were already in Pune and a write that changes nothing is not counted. $inc on a missing field creates it, so leaveTaken becomes 1. Filter updateOne on a unique field such as _id.
// emp
{ _id: 1, name: "Asha", dept: "Sales", city: "Pune" }
{ _id: 2, name: "Ravi", dept: "Sales", city: "Indore" }
{ _id: 3, name: "Meena", dept: "Sales", city: "Pune" }
{ _id: 4, name: "Kiran", dept: "IT", city: "Pune" }
db.emp.updateMany({ dept: "Sales" }, { $set: { city: "Pune" } })
db.emp.updateOne({ _id: 2 }, { $inc: { leaveTaken: 1 } })
db.emp.updateOne({ _id: 2 }, { $unset: { city: "" } })
db.emp.replaceOne({ _id: 4 }, { name: "Kiran", dept: "HR" })10. What does upsert do, and what happens when you run this update twice on an empty collection?
Upsert means update if a match exists, otherwise insert. The first run finds nothing, so it inserts a document built from the equality fields of the filter plus the update: sku PEN-BLUE, store Kochi, qty 50 and reorderAt 20, with a new ObjectId as _id. The second run matches that document, so qty becomes 100, while $setOnInsert is skipped because it applies only when inserting. Put a unique index on the filter fields (sku and store): without it, two requests at the same moment can both insert and leave duplicate stock records.
// run this twice on an empty stock collection
db.stock.updateOne(
{ sku: "PEN-BLUE", store: "Kochi" },
{ $inc: { qty: 50 }, $setOnInsert: { reorderAt: 20 } },
{ upsert: true }
)11. How do deleteOne, deleteMany and drop differ?
deleteOne removes one document that matches the filter, so give it a unique filter such as _id. deleteMany removes every match, and deleteMany({}) empties the collection but keeps it, its indexes and its validation rules. drop() removes the collection itself with all its indexes, which is faster for throwing away everything but means recreating the indexes. findOneAndDelete removes one document and returns it, handy for a simple job queue. Before a large delete in production, run countDocuments with the same filter, and remember there is no undo without a backup.
12. How do you query an array field in MongoDB?
A plain value matches if any element equals it, so A returns jobs 1 and 3. An array as the value asks for an exact match, the same elements in the same order, so B returns nothing: job 3 has the two skills in the other order and job 1 has an extra one. $all asks for all the listed elements in any order, so C returns 1 and 3. $size matches an exact length, so D returns 2 and 3; it cannot take a range such as $gt, so store a count field if you need that. Most skill searches want the A or C form, not B.
// jobs
{ _id: 1, title: "Backend", skills: ["node", "mongodb", "redis"] }
{ _id: 2, title: "Analyst", skills: ["sql", "excel"] }
{ _id: 3, title: "Full Stack", skills: ["mongodb", "node"] }
db.jobs.find({ skills: "mongodb" }) // A
db.jobs.find({ skills: ["node", "mongodb"] }) // B
db.jobs.find({ skills: { $all: ["node", "mongodb"] } }) // C
db.jobs.find({ skills: { $size: 2 } }) // D13. What is the difference between dot notation and matching a whole embedded document?
Dot notation tests one field inside the embedded document, so A returns all three users. Giving the whole embedded document asks for an exact match: the same fields, the same values, no extra fields and even the same field order. B returns only Asha, because Ravi's address has an extra area field and Neha's pin differs. With two dot-notation conditions, which is usually what you mean, C returns Asha and Ravi. Prefer dot notation in real code; exact embedded matches break as soon as someone adds a field.
// users
{ _id: 1, name: "Asha", address: { city: "Pune", pin: "411001" } }
{ _id: 2, name: "Ravi",
address: { city: "Pune", pin: "411001", area: "Baner" } }
{ _id: 3, name: "Neha", address: { city: "Pune", pin: "411045" } }
db.users.find({ "address.city": "Pune" }) // A
db.users.find({ address: { city: "Pune", pin: "411001" } }) // B
db.users.find({ "address.city": "Pune",
"address.pin": "411001" }) // C14. How does MongoDB treat null and missing fields in a query?
An equality match on null matches both a stored null and a missing field, so A returns Ravi and Neha. $exists looks only at whether the field is present: B returns only Neha, and C returns Asha and Ravi, because a field holding null still exists. To find a stored null alone, test the BSON type: D returns only Ravi. This matters in clean-up jobs and reports: a list of contacts without email built on { email: null } includes both kinds, which may or may not be what the business asked for.
// contacts
{ _id: 1, name: "Asha", email: "asha@example.com" }
{ _id: 2, name: "Ravi", email: null }
{ _id: 3, name: "Neha" }
db.contacts.find({ email: null }) // A
db.contacts.find({ email: { $exists: false } }) // B
db.contacts.find({ email: { $exists: true } }) // C
db.contacts.find({ email: { $type: "null" } }) // D15. How should you store rupee amounts in MongoDB, and why not as ordinary numbers?
A number with a fraction such as 0.1 is stored as a 64-bit double, which cannot hold most decimal fractions exactly, so 0.1 plus 0.2 becomes 0.30000000000000004. For money use Decimal128, which keeps decimal digits exactly, or store whole paise as a 64-bit integer (Rs 249.50 becomes 24950). Both keep totals and comparisons exact. Pick one style per field and stick to it, because a field that mixes doubles and decimals makes reports hard to trust. Round to two decimal places only when you display the value.
db.wallet.insertOne({ _id: 1, balance: 0.1 })
db.wallet.updateOne({ _id: 1 }, { $inc: { balance: 0.2 } })
// balance: 0.30000000000000004 (a double)
db.wallet.insertOne({ _id: 2, balance: Decimal128("0.1") })
db.wallet.updateOne({ _id: 2 },
{ $inc: { balance: Decimal128("0.2") } })
// balance: Decimal128("0.3")16. What is an index in MongoDB, and how do you know a query needs one?
An index is a sorted B-tree of field values that points to documents, so the server can jump to the matching entries instead of reading the whole collection. Every collection has a unique index on _id that cannot be dropped. Check a query with explain: a COLLSCAN stage, or totalDocsExamined far above nReturned, means it reads documents only to throw them away. After createIndex on customerId the plan shows IXSCAN. Indexes are not free: each uses RAM and disk and slows every write a little, so add them for real query patterns.
db.orders.find({ customerId: 1042 }).explain("executionStats")
// before: COLLSCAN - every document is read
db.orders.createIndex({ customerId: 1 })
// after: IXSCAN on the index "customerId_1" - only the
// matching index entries and their documents are read
db.orders.getIndexes()17. When should you embed related data in one document, and when should you reference another collection?
Embed when the data belongs to the parent, is read with it and stays small: an order's line items and shipping address are written once and always shown with the order, so one read returns everything and one update is atomic. Reference when the data is shared, changes on its own or grows without limit: a customer is used by many orders and edited separately, so the order keeps only customerId. A useful rule is that data read together should be stored together. The common mistake is copying SQL tables one to one and then needing $lookup on every request.
// one order: line items embedded, customer referenced
{
_id: "ORD-7781",
customerId: 501,
placedAt: ISODate("2025-03-14T10:20:00Z"),
items: [
{ sku: "TEA-250", name: "Assam Tea", qty: 2, price: 180 },
{ sku: "RICE-5", name: "Sona Masoori", qty: 1, price: 420 }
],
shipTo: { city: "Hyderabad", pin: "500081" }
}18. What is a replica set, and what happens when the primary fails?
A replica set is a group of mongod servers holding the same data, normally three. One primary takes all writes and records them in its oplog; the secondaries copy and apply the oplog, so they run slightly behind. If the primary is unreachable for the election timeout (10 seconds by default), the other members hold an election and a secondary becomes the new primary, usually within seconds. Drivers find the new primary on their own. A replica set gives high availability and redundancy, but it is not a backup: a bad delete is copied to every member.
19. What is the aggregation pipeline, and what does this pipeline return?
A pipeline is a list of stages, and each stage takes the documents from the one before and passes its output on, like a factory line. Here $match keeps the four paid orders, $group makes one output document per city with the sum of amount and a count from $sum: 1, and $sort orders them by total, highest first. The result is Mumbai with total 2300 and 2 orders, then Pune with 1500 and 2 orders. In SQL terms this is WHERE, GROUP BY and ORDER BY. Put $match first, so it can use an index and later stages see fewer documents.
// orders
{ _id: 1, city: "Pune", status: "paid", amount: 1200 }
{ _id: 2, city: "Mumbai", status: "paid", amount: 800 }
{ _id: 3, city: "Pune", status: "pending", amount: 500 }
{ _id: 4, city: "Pune", status: "paid", amount: 300 }
{ _id: 5, city: "Mumbai", status: "paid", amount: 1500 }
db.orders.aggregate([
{ $match: { status: "paid" } },
{ $group: { _id: "$city",
total: { $sum: "$amount" },
orders: { $sum: 1 } } },
{ $sort: { total: -1 } }
])20. What is sharding, and what are the parts of a sharded cluster?
Sharding splits one collection across several servers, called shards, so a data set too large or too busy for one replica set can grow horizontally. Each shard is its own replica set. Applications connect to mongos routers, which send each query to the right shards, and a config server replica set records which ranges of the shard key live on which shard. The shard key is a field, or fields, in every document that decides where it goes, and a background balancer moves ranges between shards to keep data even. Choosing the shard key is the key decision.
Ready to check yourself? Take the free MongoDB mock test
MongoDB interview questions: Medium questions (1-3 years)
20 questions21. How does a compound index work, and what is the ESR guideline for ordering its fields?
A compound index sorts by the first field, then by the second inside it, and so on, so it serves queries on any prefix: status; status and createdAt; or all three. It cannot efficiently serve a query on createdAt alone. ESR says put Equality fields first, then the Sort field, then Range fields. With status, createdAt, amount the server jumps to paid orders, reads them already in date order and drops small amounts inside the index scan, with no in-memory sort. If amount came before createdAt, the range would break the sort order and a blocking SORT stage would appear.
db.orders.createIndex({ status: 1, createdAt: -1, amount: 1 })
// Equality, Sort, Range: this index fits
db.orders.find({ status: "paid", amount: { $gt: 1000 } })
.sort({ createdAt: -1 })
// no status in the filter: this index does not help
db.orders.find({ createdAt: { $gte: ISODate("2025-04-01") } })22. How do you read explain("executionStats") output, and what is wrong with this query?
Compare nReturned, totalKeysExamined and totalDocsExamined; in a good plan they are close. Here the server used an index on city, read 18,000 Pune orders and threw away all but 120 paid ones, so it examined 150 documents for every one it returned. The FETCH stage with a filter on status shows the waste. A compound index on { city: 1, status: 1 } lets the index match both fields, and all three numbers drop to about 120. Also watch for COLLSCAN, a SORT stage and a high executionTimeMillis, and test with realistic data volumes, not ten rows.
db.orders.find({ city: "Pune", status: "paid" })
.explain("executionStats")
// executionStats (trimmed):
// nReturned: 120
// totalKeysExamined: 18000
// totalDocsExamined: 18000
// stages: IXSCAN { city: 1 } -> FETCH (filter: status)23. What is a multikey index, and what is the limit on compound multikey indexes?
When an indexed field holds an array, MongoDB makes the index multikey automatically: it stores one entry per element, so a search for jobs with skill mongodb can use it. There is no special syntax. The cost is size, because a document with 30 skills adds 30 entries. A compound index may include at most one array field per document, since indexing two arrays together would need every combination; that is why the last createIndex fails here, and an insert with both arrays fails once such an index exists. Multikey indexes also cannot cover a query on the array field.
// jobs
{ _id: 1, title: "Backend", skills: ["node", "mongodb"],
cities: ["Pune", "Noida"] }
db.jobs.createIndex({ skills: 1 }) // multikey
db.jobs.createIndex({ skills: 1, title: 1 }) // fine
db.jobs.createIndex({ skills: 1, cities: 1 }) // fails: two arrays24. What is a covered query, and why does the second query here miss out?
A query is covered when the filter and every returned field are in one index, so the server answers from the index alone and never reads the documents; explain shows totalDocsExamined 0 and no FETCH stage. The first query qualifies: it filters on empCode and returns only name, both in the index. The second also returns _id, which is not in this index, so every match needs a document fetch. Covered queries help most on hot lookups over large collections. They are lost if any returned field is missing from the index, or if that field is an array.
db.employees.createIndex({ empCode: 1, name: 1 })
db.employees.find({ empCode: "PUN-0042" },
{ name: 1, _id: 0 })
// covered: totalDocsExamined 0
db.employees.find({ empCode: "PUN-0042" },
{ name: 1 })
// _id is returned too, so each match is fetched25. How does $lookup work, and what does this pipeline return?
$lookup is a left outer join: for each customer it finds the orders whose custId equals the customer's _id and puts them in an array named orders, which is empty when none match. $size counts that array and $sum adds the amount values inside it, so the result is Priya 2 orders and 1350, Rahul 0 and 0, Sneha 1 and 1200. If you add $unwind on orders, Rahul disappears, because an empty array produces no output. Index orders.custId, since the lookup runs once per input document, and filter before the join so fewer documents reach it.
// customers: { _id: 1, name: "Priya" }
// { _id: 2, name: "Rahul" }
// { _id: 3, name: "Sneha" }
// orders: { _id: 101, custId: 1, amount: 900 }
// { _id: 102, custId: 1, amount: 450 }
// { _id: 103, custId: 3, amount: 1200 }
db.customers.aggregate([
{ $lookup: { from: "orders", localField: "_id",
foreignField: "custId", as: "orders" } },
{ $project: { name: 1,
orderCount: { $size: "$orders" },
spent: { $sum: "$orders.amount" } } },
{ $sort: { _id: 1 } }
])26. A report built with $unwind shows fewer posts than the collection has. Why?
$unwind outputs one document per array element, and by default nothing at all for a document whose array is empty, null or missing. So A gives 3 documents: two for post 1, one for post 2 and none for posts 3 and 4. With preserveNullAndEmptyArrays: true those two posts are kept, so B gives 5. Pipeline C counts tags and returns mongodb 2 and nosql 1; the second sort key, _id, makes the order certain if counts ever tie. Use the preserve option, or count from the original documents, when a report must include posts without tags.
// posts
{ _id: 1, tags: ["mongodb", "nosql"] }
{ _id: 2, tags: ["mongodb"] }
{ _id: 3, tags: [] }
{ _id: 4 }
db.posts.aggregate([ { $unwind: "$tags" } ]) // A
db.posts.aggregate([ { $unwind: { path: "$tags",
preserveNullAndEmptyArrays: true } } ]) // B
db.posts.aggregate([
{ $unwind: "$tags" },
{ $group: { _id: "$tags", n: { $sum: 1 } } },
{ $sort: { n: -1, _id: 1 } }
]) // C27. Why does a range query on an array return documents with no element in the range, and how does $elemMatch fix it?
On an array, each condition is tested on its own, and different elements may satisfy different conditions. For Asha, 91 is above 80 and 72 is below 85, so she matches although no score lies between them; A returns Asha and Ravi. $elemMatch demands that one element meet all the conditions together, so B returns only Ravi, whose 83 fits. The same trap applies to arrays of documents: a filter on items.sku and items.qty can match the sku in one item and the qty in another. Wrap such conditions in $elemMatch.
// results
{ _id: 1, name: "Asha", scores: [72, 91] }
{ _id: 2, name: "Ravi", scores: [83, 60] }
{ _id: 3, name: "Neha", scores: [55, 65] }
db.results.find({ scores: { $gt: 80, $lt: 85 } }) // A
db.results.find({
scores: { $elemMatch: { $gt: 80, $lt: 85 } }
}) // B28. How do you update elements inside an array without rewriting the whole document?
Use the array update operators, run in order here. $addToSet with $each adds only values not already present, so coupons becomes NEW10 and FEST5 with no duplicate. The positional $ points at the first element matched by the filter, so RICE goes to 3. $pull removes every element matching a condition, so DAL is removed. $[i] with arrayFilters updates every element that passes the filter; after the earlier steps only RICE gets gift: true, since TEA has qty 1. Each statement is atomic on the document, unlike reading the cart, editing it in code and saving it back.
// carts: { _id: 7, coupons: ["NEW10"], items: [
// { sku: "TEA", qty: 1 },
// { sku: "RICE", qty: 2 },
// { sku: "DAL", qty: 0 } ] }
db.carts.updateOne({ _id: 7 },
{ $addToSet: { coupons: { $each: ["NEW10", "FEST5"] } } })
db.carts.updateOne({ _id: 7, "items.sku": "RICE" },
{ $inc: { "items.$.qty": 1 } })
db.carts.updateOne({ _id: 7 },
{ $pull: { items: { qty: 0 } } })
db.carts.updateOne({ _id: 7 },
{ $set: { "items.$[i].gift": true } },
{ arrayFilters: [ { "i.qty": { $gte: 2 } } ] })29. How would you generate sequential invoice numbers safely in MongoDB?
Keep one counter document per series and change it with findOneAndUpdate and $inc. An update to one document is atomic, so two requests at the same moment always get different numbers, and returnNewDocument: true returns the new value; the first call creates the counter with seq: 1. The broken version reads the counter, adds one in code and saves it, which lets two requests get the same number. Expect gaps if a later step fails after taking a number. Use a counter only when the business needs a readable running series, such as invoices per financial year.
db.counters.findOneAndUpdate(
{ _id: "invoice-2025-26" },
{ $inc: { seq: 1 } },
{ upsert: true, returnNewDocument: true }
)
// first call returns { _id: "invoice-2025-26", seq: 1 }30. Why does skip and limit pagination get slow on deep pages, and what is the alternative?
skip does not jump: the server still walks past every skipped entry, so page 500 at 20 per page reads 9,980 entries just to throw them away, and each deeper page costs more. Keyset (range) pagination remembers where the last page ended and asks for what comes after it, so every page costs the same with an index on { city: 1, _id: -1 }. If you sort by a field that can repeat, such as createdAt, add _id as a second sort key and use both in the condition, or rows with equal dates can be skipped or shown twice. You lose jumping straight to page 500.
// page 500 at 20 per page: walks past 9,980 entries
db.jobs.find({ city: "Pune" })
.sort({ _id: -1 }).skip(9980).limit(20)
// keyset: pass the last _id of the previous page
db.jobs.find({ city: "Pune", _id: { $lt: lastId } })
.sort({ _id: -1 }).limit(20)
db.jobs.createIndex({ city: 1, _id: -1 })31. How does a text index work, and when is it not the right tool?
A text index splits string fields into words, drops common stop words and reduces words to a stem using language rules (English by default), so a search for developer also finds developers. $text searches it: the terms are ORed, a minus sign excludes a term, and textScore ranks the results, with title words weighted five times description words here. A collection can have only one text index, though it may cover several fields. It does not do partial words, typo tolerance or autocomplete; on MongoDB Atlas, use Atlas Search for those.
db.jobs.createIndex(
{ title: "text", description: "text" },
{ weights: { title: 5, description: 1 } }
)
db.jobs.find(
{ $text: { $search: "java developer -intern" } },
{ title: 1, score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })32. How does a TTL index work, and why might a document live past its expiry time?
A TTL index is a single-field index on a date field with expireAfterSeconds. A background task on the primary deletes a document once its date plus that many seconds has passed, and the deletes replicate like any others. The task runs every 60 seconds and can fall behind under load, so a seat hold may survive a minute or more after it expires; when timing matters, also check the expiry in your query. The field must hold a BSON date, or an array of dates where the earliest counts. A document whose field is a string never expires.
// seat holds expire 10 minutes after createdAt
db.seat_holds.createIndex({ createdAt: 1 },
{ expireAfterSeconds: 600 })
db.seat_holds.insertOne({ show: "S-21", seat: "F12",
createdAt: new Date() })
// or expire at a time stored in each document
db.passes.createIndex({ expiresAt: 1 },
{ expireAfterSeconds: 0 })33. Why does the second insert fail on a unique email index, and how do you fix it?
A unique index stores a missing field as null, so once one user without email exists, a second one is a duplicate null and fails with E11000. A partial index fixes it: with a partialFilterExpression that email is a string, only documents with a real email are indexed, so uniqueness applies to them and any number of users may leave the field out. A sparse unique index also handles the missing case. A partial index is used only when the query includes its filter condition or a stricter one. Also normalise emails, for example to lower case, before saving.
db.users.createIndex({ email: 1 }, { unique: true })
db.users.insertOne({ name: "Asha" }) // ok, no email
db.users.insertOne({ name: "Ravi" }) // E11000 duplicate key
db.users.dropIndex("email_1")
db.users.createIndex({ email: 1 }, {
unique: true,
partialFilterExpression: { email: { $type: "string" } }
})34. How do you enforce a schema in MongoDB, and what do validationLevel and validationAction control?
A validator, usually a $jsonSchema, is checked on inserts and updates; here a document without joinDate, or with joinDate as a string, is rejected. validationLevel strict, the default, checks all inserts and updates, while moderate checks inserts and updates to documents that already pass, which helps when old data is messy. validationAction error, the default, rejects the write, while warn only logs it. Add a validator to an existing collection with collMod; current documents are not rechecked until written. Since MongoDB 5.0 the error says which rule failed.
db.createCollection("employees", {
validator: { $jsonSchema: {
bsonType: "object",
required: ["empCode", "name", "joinDate"],
properties: {
empCode: { bsonType: "string",
pattern: "^EMP[0-9]+$" },
name: { bsonType: "string", minLength: 2 },
joinDate: { bsonType: "date" },
deptId: { bsonType: "objectId" }
}
} },
validationLevel: "strict",
validationAction: "error"
})35. What is write concern, and what do w: 1, w: "majority" and j: true mean?
Write concern is how much confirmation a write waits for. w: 1 returns once the primary has applied it; w: "majority" waits until a majority of the voting data-bearing members have it, so it survives a failover; j: true also waits for the on-disk journal. Since MongoDB 5.0 the default is majority for most replica sets. A w: 1 write acknowledged by a primary that then crashes can be rolled back. wtimeout ends the wait, not the write: a timed-out write may still succeed, so make retries safe. Use majority for payments and orders.
36. What is read preference, and what is the risk of reading from secondaries?
Read preference picks which members serve reads: primary (the default), primaryPreferred, secondary, secondaryPreferred or nearest. Secondaries apply the oplog after the primary, so a secondary read can return data that is seconds old, or more under lag; a user who edits a profile and then reads from a secondary may see the old value. Use secondaries for reports, analytics and nearby-region reads where slight staleness is fine, with maxStalenessSeconds to skip badly lagging members. Do not use them just to add read capacity; fix indexes first.
37. How does the size of a one-to-many relationship change your modelling choice?
Think one-to-few, one-to-many and one-to-very-many. One-to-few, such as a customer's two addresses, embeds in the parent. One-to-many in the hundreds, such as the parts of a machine, can be an array of references in the parent. One-to-very-many, such as a device's millions of log lines, must put the parent id in each child, because a parent array would grow without limit and hit the 16 MB document cap; growing arrays also make every update and index entry heavier. Then check the reads: what is shown together on one screen should usually be stored together.
38. An API looks up a job by the id in the URL and always gets null, although the job exists. What is the likely bug?
The URL gives a string, but the stored _id is an ObjectId, and values of different BSON types are never equal, so the filter matches nothing. Convert the string with new ObjectId(id) after checking that it is 24 hex characters, and return 400 for a bad id instead of letting the constructor throw. The same type rule causes other silent misses: a field saved as the string "25" from a form does not match the number 25, and { age: { $gt: 20 } } skips it. Mongoose casts ids for you; the plain driver does not, so validate types at the API boundary.
// Node.js route GET /api/jobs/:id
// broken: a string never equals an ObjectId
await jobs.findOne({ _id: req.params.id }); // null
// fixed: validate, then convert
const { ObjectId } = require("mongodb");
const id = req.params.id;
if (!/^[0-9a-f]{24}$/i.test(id)) {
return res.status(400).json({ error: "bad id" });
}
const job = await jobs.findOne({ _id: new ObjectId(id) });39. How do you compare two fields of the same document in a query?
A normal filter compares a field with a constant, so { spent: { $gt: "$budget" } } compares with the literal text $budget and matches nothing. $expr lets you use aggregation expressions, where "$spent" means the value of that field. A returns only Sales, which spent more than its budget; B returns Sales and HR, because HR spent exactly its budget. An index cannot narrow a field-to-field comparison, so on a large collection add an indexed condition such as the financial year, or store a computed flag like overBudget when the document is written.
// budgets
{ _id: 1, dept: "Sales", budget: 500000, spent: 620000 }
{ _id: 2, dept: "IT", budget: 900000, spent: 450000 }
{ _id: 3, dept: "HR", budget: 200000, spent: 200000 }
db.budgets.find({ $expr: { $gt: ["$spent", "$budget"] } }) // A
db.budgets.find({ $expr: { $gte: ["$spent", "$budget"] } }) // B40. What is NoSQL injection, and how can this login handler be bypassed?
When an app passes request JSON straight into a filter, an attacker can send an operator instead of a value. Here password becomes { $ne: "" }, which matches any non-empty password, so the attacker logs in as admin without knowing it. Fix it by checking that each input is a string before it reaches a query and by validating request bodies against a schema. Better still, never compare passwords in the query: store a bcrypt or argon2 hash, fetch the user by username and verify the hash in code. Also turn on authentication in self-managed MongoDB; it is off by default.
// Express login handler (vulnerable)
app.post("/login", async (req, res) => {
const user = await users.findOne({
username: req.body.username,
password: req.body.password
});
if (user) return res.json({ ok: true });
res.status(401).end();
});
// the attacker posts this JSON body:
// { "username": "admin", "password": { "$ne": "" } }Ready to check yourself? Take the free MongoDB mock test
MongoDB interview questions: Hard questions (experienced)
20 questions41. How do you choose a shard key for a large payments collection?
Look for high cardinality, so the data can split into many ranges; low frequency, so no single value such as one huge merchant holds a big share; and no steady increase, because a key like createdAt or an ObjectId sends every insert to the shard owning the top range. Most important, hot queries should include the key so mongos can target one shard. For payments, { merchantId: 1, createdAt: 1 } often fits: lookups are per merchant and the date splits big merchants. Changing it later is costly: reshardCollection (MongoDB 5.0+) rewrites the collection.
42. What is the difference between ranged and hashed sharding, and between targeted and scatter-gather queries?
Ranged sharding keeps nearby key values together, so a range query on the key touches few shards, but a steadily rising key piles all inserts onto one shard. Hashed sharding distributes by a hash of the key, which spreads even sequential values evenly, at the price that a range query on the key must ask every shard. mongos targets a query only when the filter includes the shard key or its prefix; otherwise it sends it to all shards and merges the replies. That is fine for rare admin reports but costly on hot paths, because the slowest shard sets the response time.
sh.shardCollection("pay.txns", { merchantId: 1, createdAt: 1 })
sh.shardCollection("app.events", { deviceId: "hashed" })
// has the shard key prefix: sent only to the owning shards
db.txns.find({ merchantId: "M-881",
createdAt: { $gte: ISODate("2025-04-01") } })
// no shard key: sent to every shard (scatter-gather)
db.txns.find({ status: "failed" })43. A Bengaluru SaaS firm must keep Indian customers' data on servers in India and EU customers' data in the EU. How can sharding help?
Zone sharding pins ranges of the shard key to chosen shards. Make region the first field of the shard key, tag the Mumbai shard as zone IN and the Frankfurt shard as EU, and assign each region's key range to its zone; the balancer then moves those ranges to the right shards and keeps them there, and new inserts land there too. A zone range includes its lower bound and excludes its upper bound. Zones place only the cluster data: backups, logs, analytics copies and replica set members in other regions must follow the same rule, so zones are one part of a data-residency plan.
// shard key of crm.customers: { region: 1, _id: 1 }
sh.addShardToZone("shardMumbai", "IN")
sh.addShardToZone("shardFrankfurt", "EU")
sh.updateZoneKeyRange("crm.customers",
{ region: "IN", _id: MinKey }, { region: "IN", _id: MaxKey },
"IN")
sh.updateZoneKeyRange("crm.customers",
{ region: "EU", _id: MinKey }, { region: "EU", _id: MaxKey },
"EU")44. Why can't you put a unique index on email alone in a sharded users collection, and what are the options?
Each shard checks uniqueness only within its own data, so MongoDB allows a unique index on a sharded collection only when it starts with the full shard key; then equal values always live on the same shard. With shard key tenantId, a unique { tenantId, email } is allowed and gives per-tenant uniqueness, but { email } alone is rejected. For global uniqueness, keep a small collection whose _id is the email, sharded on _id, and insert into it in the same transaction as the user. The _id index is only unique per shard unless _id is the shard key, so use ObjectIds or UUIDs.
// users is sharded on { tenantId: 1 }
db.users.createIndex({ tenantId: 1, email: 1 },
{ unique: true }) // allowed
db.users.createIndex({ email: 1 },
{ unique: true }) // rejected
// global uniqueness: a separate collection sharded on _id
// { _id: "asha@example.com", tenantId: 12 }45. Walk through a multi-document transaction for a wallet transfer. What must the code get right?
Every operation passes the same session, and withTransaction commits them together or not at all; this needs a replica set or a sharded cluster. Three details matter. An update that matches nothing does not throw, so the code checks matchedCount and throws to abort when the balance is too low. withTransaction retries the whole callback on transient errors such as write conflicts, so the callback must have no side effects outside the database, like sending an SMS. Keep it short: by default a transaction is aborted after 60 seconds, and long ones pressure the cache.
// amounts in paise; accounts and ledger are collections
const session = client.startSession();
try {
await session.withTransaction(async () => {
const debit = await accounts.updateOne(
{ _id: "ACC-1001", balance: { $gte: 50000 } },
{ $inc: { balance: -50000 } }, { session });
if (debit.matchedCount !== 1) {
throw new Error("insufficient balance");
}
await accounts.updateOne({ _id: "ACC-2002" },
{ $inc: { balance: 50000 } }, { session });
await ledger.insertOne({ from: "ACC-1001",
to: "ACC-2002", amount: 50000, at: new Date() },
{ session });
}, { writeConcern: { w: "majority" } });
} finally {
await session.endSession();
}46. When should you avoid multi-document transactions in MongoDB?
When a better model makes them unnecessary. A write to one document is already atomic, including its embedded arrays, so an order with its items embedded needs no transaction. Transactions cost more than single writes: they hold snapshot data in the cache, conflicting transactions abort and must retry, and a transaction that writes to several shards adds a two-phase commit across them. Use them for rare cross-document rules, such as a transfer between two accounts or a user plus a unique email claim. If nearly every request needs one, revisit the embedding first.
47. Explain read concern levels and how a rollback can lose acknowledged writes.
Read concern sets how durable the data you read must be. local, the default, returns the member's newest data even if it could still be rolled back; majority returns only data a majority holds, so it cannot be rolled back; linearizable, on the primary only, also reflects every majority-acknowledged write completed before the read began; snapshot gives a point-in-time view in transactions. Rollback: a primary acknowledges w: 1 writes, crashes before the secondaries copy them, and a secondary is elected. When the old primary rejoins, those writes are rolled back into rollback files.
48. A user saves a profile change, the next page reads from a secondary and shows old data. How do causally consistent sessions fix this?
In a causally consistent session the driver tracks the cluster time of each operation and sends it with the next one, and the member serving a read waits until it has caught up to that time. With majority read concern and majority write concern in the same session, MongoDB guarantees read-your-own-writes, monotonic reads, monotonic writes and writes-follow-reads, even when reads go to secondaries. During lag the read waits, so latency grows instead of the data going stale. The session must be passed to every operation; a read outside it gets no guarantee.
const profiles = db.collection("profiles", {
readConcern: { level: "majority" },
writeConcern: { w: "majority" },
readPreference: "secondaryPreferred"
});
const session = client.startSession({ causalConsistency: true });
await profiles.updateOne({ _id: uid },
{ $set: { city: "Pune" } }, { session });
const p = await profiles.findOne({ _id: uid }, { session });
// p.city is "Pune": the member waits until it has the write
await session.endSession();49. How do replica set elections work, and why is a four-member set no safer than a three-member one?
A member becomes primary only with votes from a strict majority of all voting members, so the set keeps a primary while a majority can reach each other. Four members need three votes, so four members tolerate one failure, the same as three, at higher cost; five tolerate two. A set may have up to 50 members but at most 7 voting ones. To survive losing a data centre, no single site may hold a majority. Be careful with arbiters: in a primary-secondary-arbiter set, losing the secondary means majority writes can no longer be acknowledged.
voting | majority | members
members | needed | that can fail
3 | 2 | 1
4 | 3 | 1
5 | 3 | 2
7 | 4 | 350. What is the oplog window, and why does it matter for operations?
The oplog is a capped collection of every change, which secondaries replay, and its window is the time span it holds before old entries are overwritten. By default it is 5% of free disk, between 990 MB and 50 GB, so a member with 400 GB free gets 20 GB; at a peak of 1.5 GB of oplog per hour that is about 13 hours. A secondary down longer than the window cannot catch up and needs a full initial sync, and a change stream cannot resume past it. Size it for your longest planned maintenance and bulk loads, and alert when the window shrinks.
rs.printReplicationInfo() // oplog size and time window
// resize on each member (size in MB); MongoDB 4.4+ also
// keeps a minimum retention period in hours
db.adminCommand({ replSetResizeOplog: 1, size: 40960 })
db.adminCommand({ replSetResizeOplog: 1, minRetentionHours: 24 })51. How do you make a heavy aggregation pipeline fast, and what memory limits apply?
Start with $match and $sort, because they can use the collection's indexes only at the start of the pipeline, and an early $match shrinks every later stage. The optimiser also moves a $match before a $project or $addFields when it does not use the new fields, and merges $sort with a following $limit so it keeps only the top 10 rows instead of sorting everything. Each stage may use 100 MB of RAM; since MongoDB 6.0, stages such as $group and $sort spill to disk by default beyond that, which is slower. Each output document is also capped at 16 MB.
db.orders.aggregate([
{ $match: { status: "paid",
createdAt: { $gte: ISODate("2025-04-01") } } },
{ $sort: { amount: -1 } },
{ $limit: 10 },
{ $project: { _id: 0, orderNo: 1, city: 1, amount: 1 } }
])
// index { status: 1, createdAt: 1 } serves the $match;
// $sort + $limit become a top-10 sort that keeps 10 rows52. How does the MongoDB query planner choose a plan, and when would you use hint()?
For a new query shape the planner builds candidate plans from the usable indexes, runs them side by side for a short trial, and caches the winner for that shape. Later queries of the same shape reuse it, and if a cached plan starts doing far more work than when it won, the planner replans. Index or collection drops clear the cache, and it does not survive a restart. hint() forces an index, which helps when the trial keeps picking a plan that wins on small samples but loses on real data. Use it sparingly: a hint stays fixed while data changes, so fix the index design first.
53. A collection has 14 indexes and inserts are slowing down. How do you safely remove the ones you do not need?
Every index is updated on each insert and on updates to its fields, and needs RAM, so extra indexes slow writes and push useful data out of the cache. Find candidates with $indexStats, which counts uses per index since the server started; run it on every member, because the counts are per member. A prefix duplicate such as { city: 1 } is usually redundant when { city: 1, status: 1 } exists. Before dropping, hide the index (MongoDB 4.4+): the planner ignores it but it is still maintained, so if a query slows down you can unhide it at once instead of rebuilding.
db.orders.aggregate([ { $indexStats: {} } ])
// accesses.ops per index since the server started
db.orders.hideIndex("city_1") // planner ignores it,
// writes still maintain it
// watch slow queries for a week, then either
db.orders.dropIndex("city_1")
// or
db.orders.unhideIndex("city_1") // back at once, no rebuild54. How does the WiredTiger cache work, and how do you know the working set no longer fits in memory?
WiredTiger keeps recently used data and indexes uncompressed in its cache, by default the larger of 50% of (RAM minus 1 GB) or 256 MB, so 7.5 GB on a 16 GB server; the operating system file cache holds compressed pages in the rest. The working set, meaning the indexes and documents your traffic touches, should fit in that cache. Signs it does not: many pages read into the cache per second, rising disk reads, and application threads doing eviction, which shows as latency spikes. Fixes are better indexes, smaller documents, archiving cold data, more RAM or sharding.
55. A Hyderabad power utility stores one reading per minute from 10,000 smart meters. How would you model it?
One document per reading means 14.4 million new documents a day, each with its own index entries. The bucket pattern groups readings, for example one document per meter per hour holding 60 readings, which cuts that to 240,000 documents a day and shrinks the indexes. Since MongoDB 5.0 a time series collection does this for you: you insert one reading at a time and the server stores them in compressed buckets per meter and time span. expireAfterSeconds drops old data. Choose a metaField that identifies the source and rarely changes, such as the meter id.
db.createCollection("meter_readings", {
timeseries: {
timeField: "ts",
metaField: "meterId",
granularity: "minutes"
},
expireAfterSeconds: 7776000 // 90 days
})
db.meter_readings.insertOne({ meterId: "HYD-00417",
ts: new Date(), kwh: 0.42, voltage: 231 })56. How do the subset and extended reference patterns use duplication, and what does it cost?
Both copy a little data to avoid a join on hot reads. The subset pattern keeps inside the parent only what the main page shows: here the product holds its 10 newest reviews, because $push with $slice: -10 trims the array, while every review also goes into a reviews collection for the full list. The extended reference copies a few stable fields, such as a customer's name and city, into each order beside customerId. The cost is keeping copies in sync: decide which must follow changes, through a background job or change stream, and which are frozen on purpose, like the address on a past invoice.
db.products.updateOne({ _id: "SKU-44" }, {
$push: { recentReviews: {
$each: [ { by: "Ritu", stars: 4, at: new Date() } ],
$slice: -10 } },
$inc: { reviewCount: 1 }
})
db.reviews.insertOne({ productId: "SKU-44", by: "Ritu",
stars: 4, text: "Fresh and well packed", at: new Date() })57. How do you change the shape of documents on a live system with millions of documents and no downtime?
Use the schema versioning pattern. Add a schemaVersion field, teach the application to read both shapes (readName handles a v1 string and a v2 object) and to write only the new one. Then migrate old documents in small batches in the background, or upgrade each one when it is next saved. A validator with validationLevel moderate enforces v2 on inserts and on documents that already pass, without blocking updates to old v1 documents. When no v1 remains, switch to strict and delete the old code path. Avoid one giant update over millions of documents at peak hours.
// v1: { _id: 1, name: "Asha Rao" }
// v2: { _id: 2, schemaVersion: 2,
// name: { first: "Ravi", last: "Kumar" } }
function readName(doc) {
if ((doc.schemaVersion || 1) === 1) {
const [first, ...rest] = doc.name.split(" ");
return { first, last: rest.join(" ") };
}
return doc.name;
}
db.runCommand({
collMod: "customers",
validator: { $jsonSchema: {
required: ["schemaVersion", "name"],
properties: {
schemaVersion: { enum: [2] },
name: { bsonType: "object",
required: ["first", "last"] }
}
} },
validationLevel: "moderate"
})58. How would you use change streams to keep a search index in sync with an orders collection?
A change stream is a cursor over a collection's changes, read from the oplog, so it needs a replica set or sharded cluster. It reports only majority-committed changes, so you never act on a write that is later rolled back. Each event carries a resume token; save it after processing and pass it as resumeAfter after a restart, which works while the token is still in the oplog window. Saving after processing means an event can be handled twice, so make the consumer idempotent. updateLookup returns the current document, which may be newer than the event; MongoDB 6.0+ can give pre- and post-images.
// savedToken: the resume token stored by the previous run
const stream = db.collection("orders").watch(
[ { $match: { operationType: { $in: ["insert", "update"] } } } ],
{ fullDocument: "updateLookup", resumeAfter: savedToken }
);
for await (const change of stream) {
await pushToSearch(change.fullDocument);
await saveToken(change._id); // the resume token
}59. What are retryable writes, and why do you still need idempotency keys in a payments API?
Drivers built for MongoDB 4.2 and later retry a write once by default when a network error or failover interrupts it, and the server uses the session and transaction number to apply it at most once. This covers single-document writes such as insertOne, updateOne and findOneAndUpdate, not updateMany or deleteMany, and needs a replica set or sharded cluster. It does not cover your own retries: if a payment app times out and the client sends the request again, that is a new write. Store a client-generated idempotency key under a unique index, so the repeat fails as a duplicate.
60. A dashboard pipeline with $lookup is slow on a large orders collection. How do you tune it?
$lookup queries the other collection once for each input document, so first cut the input with an indexed $match on date or status, and make sure foreignField is indexed (here it is _id, which always is). Since MongoDB 5.0 you can combine localField and foreignField with a pipeline, as here, to return only name and tier instead of whole customer documents, and since 5.1 the joined collection may be sharded. If the dashboard runs this join on every page load, treat it as a modelling signal: copy the customer name into each order, or precompute the numbers on a schedule.
db.orders.aggregate([
{ $match: { createdAt: { $gte: ISODate("2025-10-01") } } },
{ $lookup: {
from: "customers",
localField: "custId",
foreignField: "_id",
pipeline: [ { $project: { name: 1, tier: 1 } } ],
as: "cust"
} },
{ $unwind: "$cust" }
])Practice questions for November 2026
New every month3 questions from the MongoDB mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
Which filter finds customers whose embedded address has city "Chennai"?
- { "address.city": "Chennai" }
- { address: { $field: "city", value: "Chennai" } }
- { "address/city": "Chennai" }
- { address: "Chennai.city" }
Show the answer
Answer: { "address.city": "Chennai" }. Dot notation reaches into embedded documents, and the dotted key must be in quotes. A filter like { address: { city: "Chennai" } } would compare the whole sub-document and fail when address has more fields.
-
What is a member with priority 0?
- A member that keeps a copy of the data and can vote, but never becomes primary
- A member that is removed from the replica set at the next restart
- A member that holds no data and exists only to vote in the elections
- A member that always becomes the primary first whenever the set holds an election
Show the answer
Answer: A member that keeps a copy of the data and can vote, but never becomes primary. Priority 0 members replicate and (by default) vote but are never elected. They suit a server in a far data centre or one with weaker hardware. Hidden and delayed members must be priority 0.
-
When do you need the pipeline form of $lookup (with let and pipeline) instead of plain localField / foreignField?
- Whenever the local field holds an ObjectId value rather than a string or a number
- Whenever the foreign collection holds more than 1,000 documents, whatever the query
- Whenever both collections are in the same database as each other
- When the join needs extra conditions, such as a date range, or must reshape results
Show the answer
Answer: When the join needs extra conditions, such as a date range, or must reshape results. The pipeline form runs a sub-pipeline per input document, so it can match on several fields, filter by date, project or even group the joined data. For a plain equality join localField / foreignField is simpler and easier to index.
Ready to apply?
There are 495 open jobs asking for MongoDB on CoreHRx right now. Create a free profile once and apply with it.