MongoDB

CRUD Operations

14 Questions

MongoDB provides insertOne() to insert a single document and insertMany() to insert multiple documents in one operation. If the _id field is omitted, MongoDB auto-generates an ObjectId. insertMany() defaults to ordered inserts — it stops at the first error. Set ordered: false for bulk inserts that continue past errors. Both operations return a result object with the inserted _id(s). The insert() method (deprecated) supported both single and array inputs but is removed in newer drivers.
// insertOne — single document
const result = await db.collection('users').insertOne({
  name: "Mayur",
  email: "mayur@example.com",
  age: 30,
  createdAt: new Date()
});
console.log(result.insertedId); // ObjectId("...")

// insertMany — multiple documents
const result2 = await db.collection('users').insertMany([
  { name: "Alice", email: "alice@example.com", age: 25 },
  { name: "Bob",   email: "bob@example.com",   age: 28 }
], { ordered: false }); // continue on error
console.log(result2.insertedCount); // 2

// Custom _id
await db.collection('settings').insertOne({
  _id: "app_config",
  theme: "dark",
  maxUsers: 1000
});

Why it matters: Core CRUD operation tested in every MongoDB interview. Understanding ordered vs unordered inserts is important for bulk data pipelines.

Real applications: ordered: false is used in ETL pipelines where partial success is acceptable and the job should continue even when some documents violate unique index constraints.

Common mistakes: Not handling MongoBulkWriteError when using insertMany() with ordered: false — individual errors are embedded in the error object.

MongoDB's find() method accepts a filter object using query operators to express conditions. The filter uses operators like $eq, $ne, $gt, $gte, $lt, $lte for comparisons, $in, $nin for array membership, $and, $or, $not, $nor for logic, and $exists, $type for field checks. An empty filter {} matches all documents. Conditions on nested fields use dot notation: "address.city": "Mumbai". Conditions on array elements check if the array contains the value.
// Comparison operators
db.users.find({ age: { $gte: 18, $lte: 65 } });
db.users.find({ status: { $ne: "inactive" } });
db.users.find({ role: { $in: ["admin", "moderator"] } });

// Logical operators
db.users.find({
  $or: [{ city: "Mumbai" }, { city: "Pune" }]
});
db.users.find({
  $and: [{ age: { $gte: 25 } }, { experience: { $gte: 3 } }]
});

// Nested field (dot notation)
db.users.find({ "address.city": "Mumbai" });

// Array contains value
db.users.find({ skills: "JavaScript" });

// $exists — field must exist and not be null
db.users.find({ email: { $exists: true, $ne: null } });

// $type — check BSON type
db.users.find({ age: { $type: "int" } });

Why it matters: Query operators are the backbone of all MongoDB reads. Interviewers expect fluency with comparison, logical, and element operators.

Real applications: $in for filtering by multiple status values, $gte/$lte for date ranges, $exists for optional fields in schema-less collections.

Common mistakes: Confusing { age: 25 } (equality) with { age: { $eq: 25 } } — they are equivalent, but mixing syntax causes confusion. Also using $and unnecessarily when conditions on different fields are implicitly ANDed.

updateOne() updates the first matching document, updateMany() updates all matching documents, and replaceOne() replaces the entire document (except _id) with a new document. Update operations use update operators like $set, $unset, $inc, $push, $pull, etc. replaceOne() does not use operators — it takes a plain replacement document and overwrites all fields. The upsert option (insert if no match) works with all three methods.
// updateOne — update first match
db.users.updateOne(
  { email: "mayur@example.com" },
  {
    $set: { age: 31, updatedAt: new Date() },
    $push: { skills: "GraphQL" }
  }
);

// updateMany — update all matches
db.users.updateMany(
  { role: "user" },
  { $set: { isVerified: false } }
);

// replaceOne — replace entire document
db.users.replaceOne(
  { _id: ObjectId("...") },
  { name: "Mayur", email: "new@example.com", age: 31 }
  // ^^ _id is preserved, everything else replaced
);

// upsert — insert if no document matches
db.users.updateOne(
  { email: "new@example.com" },
  { $set: { name: "Alice", role: "user" }, $setOnInsert: { createdAt: new Date() } },
  { upsert: true }
);

Why it matters: Tests understanding of partial vs full document updates. Using replaceOne() instead of updateOne() accidentally wipes unmentioned fields — a common data loss bug.

Real applications: updateMany() for schema migrations (adding a new field to all documents), upsert for idempotent writes in event-driven systems.

Common mistakes: Forgetting $set and using a plain object in an update — this triggers a replace operation silently, wiping all fields. Always use update operators explicitly.

MongoDB provides a rich set of update operators categorized into field updates, array updates, and bitwise operations. Field operators: $set (set field value), $unset (remove field), $inc (increment), $mul (multiply), $rename (rename field), $min/$max (update if less/greater), $setOnInsert (only on upsert insert). Array operators: $push (append), $pop (remove first/last), $pull (remove matching), $addToSet (add if not exists), $each (with $push for multiple). Positional: $ (first match), $[] (all elements), $[identifier] (filtered).
// $inc — increment views by 1
db.posts.updateOne({ _id: postId }, { $inc: { views: 1, likes: 1 } });

// $push — add to array
db.posts.updateOne({ _id: postId }, { $push: { tags: "mongodb" } });

// $addToSet — add only if not already present
db.users.updateOne({ _id: userId }, { $addToSet: { skills: "React" } });

// $pull — remove matching elements
db.users.updateOne({ _id: userId }, { $pull: { skills: "jQuery" } });

// $push with $each and $slice (keep last 5)
db.feeds.updateOne({ _id: userId }, {
  $push: { activities: { $each: [newActivity], $slice: -5 } }
});

// $unset — remove a field
db.users.updateOne({ _id: userId }, { $unset: { tempToken: "" } });

// Positional $ — update matched array element
db.orders.updateOne(
  { _id: orderId, "items.productId": "abc" },
  { $set: { "items.$.status": "shipped" } }
);

Why it matters: Update operators are asked heavily in MongoDB interviews. Mastery of $push, $pull, $addToSet, and positional operators separates intermediate from advanced developers.

Real applications: $inc for counters and analytics, $addToSet for tag systems, positional $ for updating nested array elements in order management.

Common mistakes: Using $push when $addToSet is needed — $push allows duplicate values in arrays.

MongoDB provides deleteOne() (removes first matching document), deleteMany() (removes all matching documents), and drop() (removes the entire collection including indexes). findOneAndDelete() atomically finds, removes, and returns the deleted document in a single operation — useful when you need the document data after deletion. To clear all documents while keeping the collection and its indexes, use deleteMany({}). Use drop() when you want to remove the collection entirely including all indexes.
// deleteOne — remove first match
const result = await db.collection('sessions').deleteOne({
  userId: userId,
  expiresAt: { $lt: new Date() }
});
console.log(result.deletedCount); // 0 or 1

// deleteMany — remove all expired sessions
const result2 = await db.collection('sessions').deleteMany({
  expiresAt: { $lt: new Date() }
});
console.log(result2.deletedCount); // N

// findOneAndDelete — atomic find + delete, returns document
const deletedToken = await db.collection('tokens').findOneAndDelete(
  { token: "abc123" },
  { returnDocument: "before" } // return doc before deletion
);

// Drop collection (removes collection + indexes)
await db.collection('temp_data').drop();

// Delete all documents but keep collection
await db.collection('cache').deleteMany({});

Why it matters: Tests awareness of atomic operations and the difference between removing documents vs dropping collections. Important for cleanup jobs and session management.

Real applications: TTL indexes auto-delete expired documents, findOneAndDelete for job queue consumption (pop a task atomically).

Common mistakes: Running deleteMany({}) without a filter in production — this deletes ALL documents. Always double-check the filter in deletion operations.

findOneAndUpdate() atomically finds a document, applies an update, and returns either the original or updated document in a single round-trip. This is crucial for compare-and-swap operations where you need to know the document's state before or after the update. The returnDocument: "after" option (or returnNewDocument: true in older drivers) returns the updated document. Combined with upsert: true, it can atomically create or update. This is MongoDB's solution for atomic read-modify-write without explicit transactions.
// Return UPDATED document (after update)
const updatedUser = await db.collection('users').findOneAndUpdate(
  { _id: userId },
  {
    $inc: { credits: -10 },
    $push: { purchaseHistory: { item: "Pro Plan", date: new Date() } }
  },
  {
    returnDocument: "after",  // return doc after update
    projection: { credits: 1, name: 1 }
  }
);

// Atomic job queue — "claim" a pending job
const job = await db.collection('jobs').findOneAndUpdate(
  { status: "pending" },
  { $set: { status: "processing", startedAt: new Date(), workerId: WORKER_ID } },
  { sort: { priority: -1, createdAt: 1 }, returnDocument: "after" }
);

// Upsert + findOneAndUpdate — thread-safe counter
const counter = await db.collection('counters').findOneAndUpdate(
  { _id: "order_seq" },
  { $inc: { seq: 1 } },
  { upsert: true, returnDocument: "after" }
);

Why it matters: Shows understanding of atomic operations for concurrent environments. This pattern replaces multi-step read-modify-write workflows that are prone to race conditions.

Real applications: Sequence generators, job queue systems (claim a task), inventory management (decrement stock and confirm availability atomically).

Common mistakes: Using separate findOne() + updateOne() for atomic operations — this is a race condition in concurrent systems. Always use findOneAndUpdate() for read-modify-write.

The $regex operator performs regular expression pattern matching on string fields. MongoDB supports JavaScript-compatible regex with flags: i (case-insensitive), m (multiline), x (extended), s (dotAll). Performance warning: unanchored regex (no leading ^) cannot use indexes and causes a full collection scan. Anchored regex (^prefix) can use an index. For full-text search requirements, use $text with a text index which is far more efficient than regex for word searches.
// Case-insensitive search
db.users.find({ name: { $regex: /mayur/i } });

// Anchored — uses index if name is indexed
db.products.find({ name: { $regex: /^laptop/i } });

// $regex operator with options
db.users.find({
  email: { $regex: "@gmail\.com$", $options: "i" }
});

// Inline regex (shorthand)
db.users.find({ name: /^M/i });

// $regex vs $text comparison:
// $regex /javascript/ — full collection scan (slow)
// $text: { $search: "javascript" } — uses text index (fast)

// For autocomplete, use anchored prefix search
db.products.find({ name: { $regex: `^${searchTerm}`, $options: "i" } });

// AVOID in production on large collections:
db.users.find({ bio: { $regex: /developer/ } }); // full scan!

Why it matters: Tests knowledge of regex performance implications. Using regex incorrectly on large collections is a common cause of production performance issues.

Real applications: Autocomplete search with anchored prefix regex, email validation queries, and domain filtering (e.g., find all @company.com emails).

Common mistakes: Using unanchored regex like { name: /mayur/ } on millions of documents — triggers a full collection scan. Use text indexes for full-text search instead.

Projection is the second argument to find() that specifies which fields to include or exclude in the returned documents. Use 1 to include fields and 0 to exclude. You cannot mix inclusion and exclusion (except for _id). By default, all fields are returned. Projections are a critical optimization — fetching only needed fields reduces network transfer, memory usage, and application processing. The $slice operator in projections limits returned array elements. $elemMatch returns only the first matching array element.
// Include only name, email (exclude rest)
db.users.find({}, { name: 1, email: 1 });
// Returns: { _id: ObjectId, name: "...", email: "..." }

// Exclude sensitive fields
db.users.find({}, { password: 0, ssn: 0 });

// Exclude _id (only allowed with inclusion)
db.users.find({}, { name: 1, email: 1, _id: 0 });

// $slice — return only first 3 tags
db.posts.find({}, { title: 1, tags: { $slice: 3 } });

// $elemMatch — return only matched array element
db.orders.find(
  { "items.productId": "abc" },
  { "items.$": 1 }  // positional projection
);

// Nested field projection
db.users.find({}, {
  "name": 1,
  "address.city": 1  // only address.city sub-field
});

Why it matters: Projection is a key performance optimization. Interviewers look for awareness of over-fetching data and network efficiency especially in high-throughput APIs.

Real applications: API endpoints that return user list views project only name, email, avatar — not the full 50-field document. Critical for mobile API responses.

Common mistakes: Mixing inclusion and exclusion: { name: 1, password: 0 } throws an error. You can only use one mode (except _id: 0 with inclusions).

sort() orders results by one or more fields (1 = ascending, -1 = descending). limit() caps the number of returned documents. skip() bypasses a specified number of documents (offset-based pagination). These three are typically chained in the order sort → skip → limit. MongoDB processes them in this order internally regardless of chain order. Offset-based pagination using skip has a performance problem at large offsets: MongoDB still scans all skipped documents. For large datasets, use cursor-based pagination using the last seen _id instead.
// Sort by age descending, then name ascending
db.users.find().sort({ age: -1, name: 1 }).limit(10);

// Pagination — page 3, 10 items per page
const page = 3, perPage = 10;
db.users.find().sort({ createdAt: -1 }).skip((page-1)*perPage).limit(perPage);

// ⚠️ PERFORMANCE ISSUE: skip(1000000) scans 1M docs!

// Cursor-based pagination (efficient for large collections)
// First page
const docs = await db.users.find().sort({ _id: 1 }).limit(10).toArray();
const lastId = docs[docs.length - 1]._id;

// Next page — use lastId as cursor
const nextDocs = await db.users.find({ _id: { $gt: lastId } })
  .sort({ _id: 1 }).limit(10).toArray();

// Sort by multiple fields
db.orders.find()
  .sort({ status: 1, createdAt: -1 })
  .skip(0).limit(20);

Why it matters: Pagination is a real-world requirement and a frequent interview topic. Understanding the skip performance problem and cursor-based pagination alternatives shows production readiness.

Real applications: Admin dashboards use offset pagination (small datasets), infinite scroll feeds use cursor-based pagination for consistent performance.

Common mistakes: Using large skip values on millions of records — skip(10000) scans and discards 10,000 documents every time, degrading linearly with offset size.

bulkWrite() allows executing multiple write operations (insert, update, delete, replace) in a single request, dramatically reducing round-trip overhead. It accepts an array of operation objects. By default, operations execute in ordered mode (stops on first error). Set ordered: false for unordered mode (continues past errors, potentially faster due to parallelism). bulkWrite returns a detailed BulkWriteResult with counts of each operation type. This is essential for batch processing, ETL jobs, and migrations where individual document operations would be too slow.
const result = await db.collection('products').bulkWrite([
  // Insert new product
  { insertOne: { document: { name: "Keyboard", price: 2500, stock: 100 } } },

  // Update price of existing product
  { updateOne: {
    filter: { sku: "LAPTOP-001" },
    update: { $inc: { price: 500 }, $set: { updatedAt: new Date() } }
  }},

  // Upsert — insert if not exists
  { updateOne: {
    filter: { sku: "MOUSE-002" },
    update: { $setOnInsert: { price: 800, stock: 50 } },
    upsert: true
  }},

  // Delete discontinued products
  { deleteMany: { filter: { status: "discontinued" } } },

  // Replace entire document
  { replaceOne: {
    filter: { sku: "TABLET-003" },
    replacement: { sku: "TABLET-003", name: "iPad Pro", price: 80000 }
  }}
], { ordered: false }); // continue past errors

console.log(result.insertedCount);  // 1
console.log(result.modifiedCount);  // 1
console.log(result.deletedCount);   // N

Why it matters: Tests knowledge of batch processing efficiency. Single writes per request in ETL jobs or sync operations are a common performance anti-pattern that bulkWrite solves.

Real applications: Inventory sync from ERP system updates 10,000 product prices in one bulkWrite call. Data migration scripts use bulkWrite for efficient batch transforms.

Common mistakes: Not using bulkWrite for batch operations — inserting 1000 documents with 1000 individual insertOne() calls has 1000× the network overhead vs one bulkWrite().

The $where operator allows using JavaScript expressions or functions as query filters, executing JavaScript code on the MongoDB server (via the SpiderMonkey engine). While powerful, it is extremely slow because it cannot use indexes and evaluates JavaScript for every document — a full collection scan. It is also a security risk if user input is not sanitized (NoSQL injection). MongoDB recommends using native query operators ($regex, $expr, $and, $function in aggregation) instead of $where. In MongoDB 4.4+, $function and $accumulator in aggregation pipelines are the safe replacement.
// ⚠️ AVOID — $where is slow and risky
db.users.find({ $where: "this.age > 25 && this.firstName === this.lastName" });

// ✅ BETTER — use native operators
db.users.find({ age: { $gt: 25 }, $expr: { $eq: ["$firstName", "$lastName"] } });

// $expr — compare two fields (safe, can use index)
db.orders.find({
  $expr: { $gt: ["$total", "$budget"] }
});

// $function in aggregation (MongoDB 4.4+)
db.users.aggregate([{
  $match: {
    $expr: {
      $function: {
        body: "function(name) { return name.startsWith('M'); }",
        args: ["$name"],
        lang: "js"
      }
    }
  }
}]);

Why it matters: Shows security awareness and performance knowledge. Interviewers test whether you know the security and performance pitfalls of executing arbitrary JS on the database server.

Real applications: Virtually none — $where has no valid modern use case. $expr covers all field comparison needs efficiently.

Common mistakes: Using unsanitized user input in $where expressions opens NoSQL injection vulnerabilities where attackers can execute arbitrary JavaScript on the DB server.

Write concern specifies how many replica set members must acknowledge a write before MongoDB considers it successful. w: 1 (primary acknowledges), w: "majority" (majority of replica members acknowledge — safest), w: 0 (fire and forget — no acknowledgment). Adding j: true requires the write to be journaled. Read concern controls the consistency of data read by queries. "local" returns most recent data on primary (default), "majority" reads data acknowledged by majority of replica set (prevents reading rolled-back data), "linearizable" guarantees reading the latest committed data.
// Write concern levels
await db.collection('transactions').insertOne(
  { amount: 5000, type: "debit" },
  { writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
);

// Read concern options
const highConsistencyDocs = await db.collection('accounts')
  .find({}, { readConcern: { level: "majority" } });

// Session-level write/read concern
const session = client.startSession({
  defaultTransactionOptions: {
    writeConcern: { w: "majority" },
    readConcern: { level: "snapshot" }
  }
});

// Write Concern Summary:
// w: 0    — no ack (fastest, unsafe)
// w: 1    — primary ack (default)
// w: 2    — 2 members ack
// w: "majority" — >50% members ack (safest)
// j: true — journaled (durable on disk)

// Read Concern Summary:
// "local"         — default, may see rolled-back data
// "majority"      — only committed data
// "snapshot"      — point-in-time snapshot (transactions)

Why it matters: Advanced topic testing understanding of distributed consistency trade-offs. Critical for designing systems that balance performance vs data durability.

Real applications: Financial systems use w: "majority", j: true for critical writes. Analytics use w: 0 for high-throughput event logging where occasional loss is acceptable.

Common mistakes: Using w: "majority" without a proper replica set — on a standalone instance, this will block forever waiting for acknowledgment that can never come.

$expr allows using aggregation expressions inside query operators, enabling comparisons between fields in the same document. $cond (conditional expression in aggregation) evaluates a condition and returns one of two values, similar to a ternary operator. In MongoDB 4.2+, update with aggregation pipeline allows using aggregation expressions directly in update operations, enabling conditional field updates, computing from existing field values, and complex transformations — impossible with regular update operators alone.
// $expr — compare two fields in the same document
db.orders.find({
  $expr: { $gt: ["$earned", "$spent"] } // earned > spent
});

// Update with pipeline (MongoDB 4.2+) — set bonus based on salary
db.employees.updateMany(
  {},
  [{
    $set: {
      bonus: {
        $cond: {
          if: { $gte: ["$salary", 100000] },
          then: { $multiply: ["$salary", 0.15] }, // 15% for high earners
          else: { $multiply: ["$salary", 0.10] }  // 10% for others
        }
      },
      updatedAt: "$$NOW"  // current date in pipeline
    }
  }]
);

// Conditional update — append to array only if field exists
db.users.updateOne(
  { _id: userId },
  [{ $set: {
    displayName: {
      $ifNull: ["$nickname", "$name"] // use nickname if exists, else name
    }
  }}]
);

Why it matters: Advanced update technique that shows knowledge of aggregation pipelines in updates. This feature (MongoDB 4.2+) eliminates many workarounds previously required for conditional updates.

Real applications: Salary adjustment scripts, tier-based discount calculations, and field normalization during data migrations.

Common mistakes: Not knowing that pipeline-style updates (array syntax) are different from regular update operators — you cannot mix $set operator with pipeline stage $set.

Both $push and $addToSet add elements to arrays, but $push always appends (allows duplicates) while $addToSet only adds if the element doesn't already exist (maintains a unique set). $push combined with $each can add multiple elements and with $slice can limit array size, and with $sort can maintain sorted order. $addToSet cannot be combined with $each and $sort. Use $addToSet for tags, categories, and unique collections; use $push for ordered lists and history logs.
// $push — allows duplicates
// First push
db.posts.updateOne({ _id: postId }, { $push: { tags: "mongodb" } });
// Tags: ["mongodb"]

// Second push of same value
db.posts.updateOne({ _id: postId }, { $push: { tags: "mongodb" } });
// Tags: ["mongodb", "mongodb"] ← duplicate!

// $addToSet — no duplicates
db.posts.updateOne({ _id: postId }, { $addToSet: { tags: "mongodb" } });
db.posts.updateOne({ _id: postId }, { $addToSet: { tags: "mongodb" } });
// Tags: ["mongodb"] ← no duplicate added

// $push with $each + $slice (sliding window — keep last 10)
db.users.updateOne({ _id: userId }, {
  $push: {
    recentActivity: {
      $each: [{ action: "login", at: new Date() }],
      $slice: -10, // keep last 10 entries
      $sort: { at: 1 }
    }
  }
});

// $addToSet with $each — add multiple unique values
db.users.updateOne({ _id: userId }, {
  $addToSet: { skills: { $each: ["React", "Node.js", "MongoDB"] } }
});

Why it matters: A common interview question distinguishing array operator knowledge. The duplicate problem with $push is a real production bug seen frequently.

Real applications: $addToSet for user skill tags, category lists, follower arrays. $push + $slice for activity feeds, recent items with bounded size.

Common mistakes: Using $push for a skill set that should be unique — results in ["JavaScript", "JavaScript", "JavaScript"] after multiple updates.