18 Topics
Chapter•240 Minutes

MongoDB CRUD Operations: Insert, Read, Update, and Delete

Master MongoDB CRUD operations from foundational commands to advanced querying and modification techniques. Learn insertOne, insertMany, find, findOne, projections, comparison and logical operators, array and nested-document filters, updateOne, updateMany, replacement operations, upserts, update operators, delete operations, return documents, write results, atomicity, concurrency considerations, and practical data manipulation patterns.

Understanding CRUD and MongoDB Documents

Level:Beginner to AdvancedFocus:CRUD

CRUD stands for Create, Read, Update, and Delete. These four operations form the foundation of database interaction.

MongoDB implements CRUD operations against collections of BSON documents. Each operation has its own behavior, options, return values, and failure scenarios.

1. Create

Create operations insert new documents into a collection.

Common methods:

  • insertOne() inserts one document.
  • insertMany() inserts multiple documents.

Example:

db.students.insertOne({
  name: "Arun",
  age: 22
});

2. Read

Read operations retrieve documents that match a filter.

Common methods:

  • find() returns a cursor for matching documents.
  • findOne() returns one matching document or null.

Example:

db.students.find({ age: { $gte: 18 } });

3. Update

Update operations modify existing documents.

Common methods:

  • updateOne() modifies at most one matching document.
  • updateMany() modifies all matching documents.
  • replaceOne() replaces the contents of one matching document, apart from the immutable _id behavior and other operation constraints.

Example:

db.students.updateOne(
  { name: "Arun" },
  { $set: { age: 23 } }
);

4. Delete

Delete operations remove documents that match a filter.

Common methods:

  • deleteOne() deletes at most one matching document.
  • deleteMany() deletes all matching documents.

Example:

db.students.deleteOne({ name: "Arun" });

CRUD operations are not interchangeable

A read operation does not modify stored data. An update operation can modify selected fields without replacing the entire document. A replacement operation replaces the document's non-_id contents. A delete operation removes matching documents.

Understanding these differences prevents common mistakes, particularly when updating production data.

Atomicity

MongoDB guarantees atomicity at the individual-document level. An update that modifies several fields within one document is atomic as a single-document write. Multi-document operations such as updateMany() are not atomic as a whole unless they are executed within a transaction.

This distinction matters when an application modifies related data across multiple documents.

CRUD operations
Create: insertOne, insertMany
Create new documents in a collection.
Read: find, findOne
Retrieve documents that match a filter.
Update: updateOne, updateMany, replaceOne
Modify existing documents or replace their non-identifier contents.
Delete: deleteOne, deleteMany
Remove documents matching a filter.
Key Takeaways
  • CRUD stands for Create, Read, Update, and Delete.
  • MongoDB CRUD methods operate on collections of documents.
  • Update operations can modify selected fields without replacing the whole document.
  • Replacement operations have different semantics from modifier updates.
  • MongoDB guarantees atomicity for individual-document writes, not automatically for an entire multi-document operation.
Knowledge Check

Which statement about MongoDB updateMany() is correct?

Inserting a Single Document with insertOne()

The insertOne() method inserts a single document into a collection.

Basic syntax

db.collection.insertOne(document, options)

The options argument is optional. Supported options depend on the operation and MongoDB version.

Example 1: Insert a student

db.students.insertOne({
  name: "Arun",
  age: 22,
  course: "MongoDB",
  active: true
});

If _id is omitted, MongoDB's driver or shell ordinarily generates an ObjectId for the inserted document.

The insert result includes the inserted identifier.

Example 2: Insert nested documents

db.students.insertOne({
  name: "Meera",
  age: 24,
  address: {
    city: "Chennai",
    country: "India"
  },
  skills: ["JavaScript", "Node.js", "MongoDB"]
});

MongoDB stores address as an embedded document and skills as an array.

Example 3: Supply a custom _id

db.students.insertOne({
  _id: "student-1001",
  name: "Ravi",
  age: 25
});

The _id field does not have to be an ObjectId. It must be unique within the collection.

If another document already has the same _id, the insert fails with a duplicate-key error.

Example 4: Inspect the insert result

const result = db.students.insertOne({
  name: "Kavitha",
  age: 21
});

printjson(result);

The result typically contains acknowledged: true and insertedId.

An acknowledged write indicates that the server acknowledged the operation according to the configured write concern. It should not be confused with a guarantee that every possible downstream system has processed the data.

Example 5: Insert an explicitly typed value

db.products.insertOne({
  name: "Keyboard",
  price: NumberDecimal("2499.99"),
  createdAt: new Date()
});

This example stores the price as Decimal128 and the creation time as BSON Date.

Practical considerations

  • Validate required fields before inserting data.
  • Avoid hardcoding production identifiers unless the application requires deterministic IDs.
  • Use a consistent field naming convention.
  • Store values using appropriate BSON types.
  • Handle duplicate-key and validation errors in the application.
  • Never assume an insert succeeded solely because the application sent the request.
Use the inserted identifier

When a new document is inserted, use the returned insertedId to retrieve or reference that document rather than relying on a non-unique field such as a person's name.

Insert a document and inspect the result
const result = db.students.insertOne({
  name: "Arun",
  age: NumberInt(22),
  course: "MongoDB",
  skills: ["JavaScript", "MongoDB"],
  createdAt: new Date()
});

print("Acknowledged:", result.acknowledged);
print("Inserted ID:", result.insertedId);

db.students.findOne({ _id: result.insertedId });
💡 The example inserts a document, inspects the acknowledged write result and generated identifier, and retrieves the inserted document by its identifier.
Key Takeaways
  • insertOne() inserts one document.
  • MongoDB ordinarily generates an ObjectId when _id is omitted.
  • Custom _id values are supported when unique within the collection.
  • Duplicate identifiers cause duplicate-key errors.
  • The insert result exposes the inserted identifier and write acknowledgment status.
Knowledge Check

What happens if insertOne() attempts to insert a document with an _id already present in the collection?

Inserting Multiple Documents with insertMany()

The insertMany() method inserts multiple documents in one operation.

Basic syntax

db.collection.insertMany(
  [document1, document2, document3],
  options
)

The first argument must be an array of documents.

Example: Insert a course catalog

db.courses.insertMany([
  {
    title: "MongoDB Basics",
    level: "beginner",
    durationHours: 8
  },
  {
    title: "MongoDB Aggregation",
    level: "intermediate",
    durationHours: 12
  },
  {
    title: "MongoDB Performance",
    level: "advanced",
    durationHours: 10
  }
]);

MongoDB generates identifiers for documents that do not specify _id.

Ordered inserts

By default, insertMany() uses ordered insertion behavior. If an insert fails, the operation stops attempting later documents after encountering the error, although documents successfully inserted before the error remain inserted.

For example, suppose an array contains three documents and the second document violates a unique constraint. The first may already be inserted, the second fails, and the third may not be attempted under ordered execution.

Ordered execution does not make the entire batch transactional.

Unordered inserts

You can request unordered insertion:

db.courses.insertMany(
  [
    { _id: "course-1", title: "MongoDB Basics" },
    { _id: "course-1", title: "Duplicate ID" },
    { _id: "course-2", title: "Aggregation" }
  ],
  { ordered: false }
);

The duplicate _id causes that document's insertion to fail. MongoDB can continue attempting other documents when ordered is false. The operation still reports errors for failed inserts.

The exact result details depend on the driver or shell and the errors encountered.

Batch size and memory

For large imports, avoid constructing an enormous array in application memory without considering memory limits and operational requirements.

Consider batching, streaming input, retry behavior, and the maximum BSON size of individual documents.

insertMany() is not a replacement for bulkWrite()

insertMany() is designed for inserting documents. bulkWrite() supports a mixture of insert, update, replacement, and delete operations in one bulk request.

Use the appropriate method for the workload and understand its error-handling behavior.

Bulk insertion is not automatically all-or-nothing

Successful inserts can remain even when another document in the same insertMany() operation fails. Use a transaction when an actual multi-document all-or-nothing requirement calls for one and the deployment supports it.

Compare ordered and unordered insertion
db.insertManyDemo.insertMany([
  { _id: 1, name: "Arun" },
  { _id: 2, name: "Meera" },
  { _id: 3, name: "Ravi" }
]);

// Unordered insertion attempts independent documents even if one fails.
// Run this example in a disposable practice collection.
db.insertManyDemo.insertMany(
  [
    { _id: 4, name: "Kavitha" },
    { _id: 4, name: "Duplicate ID" },
    { _id: 5, name: "Divya" }
  ],
  { ordered: false }
);

// Inspect which documents exist after the attempted batch.
db.insertManyDemo.find().sort({ _id: 1 });
💡 The second batch deliberately contains a duplicate identifier. Unordered execution continues attempting eligible operations after the failure. The final query shows the documents that were actually inserted. Duplicate errors must still be handled by the application.
Key Takeaways
  • insertMany() inserts an array of documents.
  • Ordered insertion stops attempting later documents after an insertion error.
  • Unordered insertion can continue attempting documents after individual failures.
  • A failed batch does not imply that no documents were inserted.
  • Use bulkWrite() when you need a mixture of CRUD operations in a batch.
Knowledge Check

What is a key difference between ordered and unordered insertMany() operations?

Reading Documents with find() and findOne()

MongoDB provides find() and findOne() for retrieving documents.

find()

db.users.find({ active: true });

The find() method returns a cursor representing the matching documents. The shell displays results according to its cursor-display behavior.

In application code, a cursor can be iterated or consumed using the APIs provided by the MongoDB driver.

findOne()

db.users.findOne({ email: "arun@example.com" });

findOne() returns one matching document or null if no document matches.

If several documents match, do not assume that the returned document is the newest, oldest, or otherwise preferred document unless you explicitly define a sort order.

Empty filter

db.users.find({});

An empty filter matches all documents in the collection.

This is useful for exploration, but production queries should generally include appropriate filters and pagination or limits where necessary.

Querying nested fields

db.users.find({ "address.city": "Chennai" });

Dot notation allows you to query fields inside embedded documents.

Querying arrays

db.users.find({ skills: "MongoDB" });

For an array field, this matches documents whose array contains the specified value.

Sorting results

db.users.find({ active: true }).sort({ createdAt: -1 });

The sort order -1 means descending and 1 means ascending.

For deterministic pagination, use a sort order that includes a unique tie-breaker, such as _id, especially when multiple documents may have the same primary sort value.

Limiting results

db.users.find({ active: true })
  .sort({ createdAt: -1, _id: -1 })
  .limit(20);

This returns at most 20 matching documents, ordered by creation time and identifier.

Cursors and large datasets

A cursor allows results to be processed incrementally rather than materializing every document at once. This is important when working with large collections.

In the Node.js driver, use cursor iteration or an appropriate cursor API when handling large result sets. Avoid calling toArray() on an unbounded result set if the result may exceed available application memory.

Read, sort, and limit documents
db.readDemo.insertMany([
  { name: "Arun", age: 22, active: true },
  { name: "Meera", age: 24, active: true },
  { name: "Ravi", age: 25, active: false }
]);

// Find all active users.
db.readDemo.find({ active: true });

// Retrieve one matching document.
db.readDemo.findOne({ name: "Arun" });

// Sort and limit the result set.
db.readDemo.find({ active: true })
  .sort({ age: -1, _id: -1 })
  .limit(10);
💡 The example demonstrates basic filtering, retrieving one document, and sorting with a unique tie-breaker before limiting the result set.
Key Takeaways
  • find() returns a cursor for matching documents.
  • findOne() returns one document or null.
  • An empty filter matches all documents.
  • Sorting is required when a particular result order matters.
  • Cursors help process large result sets without loading every document into memory.
Knowledge Check

What does findOne() return when no document matches its filter?

Query Filters and Comparison Operators

MongoDB query filters determine which documents are selected by a read, update, or delete operation.

A filter is a document describing conditions that documents must satisfy.

Equality

db.products.find({ category: "electronics" });

This matches documents where the category field equals the specified value.

Comparison operators

MongoDB provides comparison operators such as:

  • $eq: equal to.
  • $ne: not equal to.
  • $gt: greater than.
  • $gte: greater than or equal to.
  • $lt: less than.
  • $lte: less than or equal to.
  • $in: matches any specified value.
  • $nin: matches none of the specified values.

Greater than and less than

db.products.find({ price: { $gt: 1000 } });

This selects products with a numeric price greater than 1000, subject to MongoDB's comparison semantics and the stored field types.

Range query

db.products.find({
  price: {
    $gte: 500,
    $lte: 2000
  }
});

The query requires the price to be at least 500 and at most 2000.

$in

db.products.find({
  category: { $in: ["electronics", "books"] }
});

The query matches either category. $in is also useful for matching array fields against any of the supplied values.

$nin

db.products.find({
  category: { $nin: ["electronics", "books"] }
});

$nin matches documents where the field value is not in the supplied list. It can also match documents where the field is missing, so consider whether that behavior is appropriate for your requirement.

$ne

db.products.find({ price: { $ne: 1000 } });

$ne is not equivalent to checking that a field exists and has a different value. Depending on the query, documents with missing fields can match $ne filters.

Type awareness

If a price field contains the string "1500", do not assume that it behaves like the number 1500 in a numeric range filter. Use consistent types and inspect legacy data when query results seem unexpected.

Query selectivity

A filter that matches a small fraction of a collection may be more selective than one that matches nearly every document. Indexes and data distribution affect query performance, which will be covered in the indexing chapters.

Comparison operators
$eq: Equal
Matches values equal to the supplied value.
$ne: Not equal
Matches values unequal to the supplied value and can include documents where the field is missing.
$gt / $gte: Greater than
Compare a field with a lower threshold.
$lt / $lte: Less than
Compare a field with an upper threshold.
$in / $nin: Membership
Match values included in or excluded from a specified list.
Use comparison operators
db.filterDemo.insertMany([
  { name: "Keyboard", price: 1500, category: "electronics" },
  { name: "Book", price: 500, category: "books" },
  { name: "Monitor", price: 8000, category: "electronics" },
  { name: "Chair", price: 2500, category: "furniture" }
]);

// Price greater than 1000.
db.filterDemo.find({ price: { $gt: 1000 } });

// Inclusive price range.
db.filterDemo.find({
  price: { $gte: 500, $lte: 2500 }
});

// Match one of several categories.
db.filterDemo.find({
  category: { $in: ["electronics", "books"] }
});
💡 The sample dataset demonstrates numeric comparison, inclusive range filtering, and matching one of several categorical values.
Key Takeaways
  • Query filters define which documents an operation affects.
  • Comparison operators support equality, inequalities, and membership tests.
  • $in matches any specified value.
  • $ne and $nin may match documents where a field is missing.
  • Consistent BSON types are important for predictable filtering.
Knowledge Check

What does `{ price: { $gte: 500, $lte: 2000 } }` mean?

Logical Operators: $and, $or, $nor, and $not

MongoDB supports logical operators for combining or negating query conditions.

$and

$and matches documents that satisfy every condition in the supplied array.

db.products.find({
  $and: [
    { price: { $gte: 1000 } },
    { stock: { $gt: 0 } }
  ]
});

This selects products priced at least 1000 that also have positive stock.

MongoDB often allows implicit AND when multiple conditions are expressed as separate fields in the same filter:

db.products.find({
  price: { $gte: 1000 },
  stock: { $gt: 0 }
});

The explicit form is useful when necessary to express multiple conditions involving the same field or more complicated logical structures.

$or

$or matches documents satisfying at least one condition.

db.products.find({
  $or: [
    { category: "books" },
    { price: { $lt: 500 } }
  ]
});

A product matches if it is a book, costs less than 500, or satisfies both conditions.

$nor

$nor matches documents that fail all listed query expressions.

db.products.find({
  $nor: [
    { category: "electronics" },
    { stock: { $lt: 1 } }
  ]
});

This excludes documents matching either listed condition. Missing fields can affect how individual expressions evaluate, so inspect the intended behavior for optional fields.

$not

$not is a field-level operator used to negate the result of another operator or regular expression.

db.products.find({
  price: { $not: { $gt: 1000 } }
});

This matches documents for which the price does not satisfy the greater-than condition, including some documents where the field is missing. It is not always interchangeable with $lte.

Combine logical and comparison operators

db.products.find({
  $or: [
    {
      category: "electronics",
      price: { $lt: 5000 }
    },
    {
      category: "books",
      stock: { $gt: 0 }
    }
  ]
});

Each object inside $or can contain multiple conditions. Those conditions are combined with AND within that object, while the outer $or requires at least one object to match.

Practical query design

Before using nested logical operators, write the requirement in plain language. Then translate each condition into a filter and verify it against a small dataset.

Combine logical conditions
db.logicalDemo.insertMany([
  { name: "Keyboard", category: "electronics", price: 1500, stock: 10 },
  { name: "Book", category: "books", price: 400, stock: 5 },
  { name: "Monitor", category: "electronics", price: 8000, stock: 0 },
  { name: "Notebook", category: "stationery", price: 100, stock: 20 }
]);

// AND: both conditions must match.
db.logicalDemo.find({
  price: { $gte: 500 },
  stock: { $gt: 0 }
});

// OR: either condition can match.
db.logicalDemo.find({
  $or: [
    { category: "books" },
    { price: { $lt: 500 } }
  ]
});

// NOR: exclude documents matching either expression.
db.logicalDemo.find({
  $nor: [
    { category: "electronics" },
    { stock: { $lt: 1 } }
  ]
});
💡 The examples demonstrate implicit AND, explicit OR, and NOR. Carefully examine the returned documents to see how conditions combine.
Key Takeaways
  • $and requires every listed condition to match.
  • $or requires at least one listed condition to match.
  • $nor excludes documents matching any listed condition.
  • $not negates a field-level query expression and is not always equivalent to a simpler comparison operator.
  • Nested conditions within one $or branch are combined with AND.
Knowledge Check

In an $or array, what happens when one condition object contains both `category: "books"` and `stock: { $gt: 0 }`?

Projection: Selecting Fields in Query Results

Projection controls which fields MongoDB returns from matching documents. It can reduce response size and prevent unnecessary fields from being exposed to application code.

Include specific fields

db.users.find(
  { active: true },
  { name: 1, email: 1 }
);

The query returns matching documents with name, email, and _id by default.

Exclude the identifier

db.users.find(
  { active: true },
  { name: 1, email: 1, _id: 0 }
);

The _id field is included by default unless explicitly excluded.

Exclude a field

db.users.find(
  {},
  { passwordHash: 0 }
);

This excludes passwordHash while retaining other fields.

Inclusion and exclusion rules

Ordinarily, you cannot mix inclusion and exclusion projections in the same projection, apart from excluding _id in an inclusion projection.

For example, this is a valid inclusion projection:

{ name: 1, email: 1, _id: 0 }

This is generally invalid as an ordinary find projection:

{ name: 1, passwordHash: 0 }

Nested-field projection

db.users.find(
  {},
  { name: 1, "address.city": 1, _id: 0 }
);

This includes the name and the nested city field.

Array projection operators

MongoDB offers specialized projection mechanisms for arrays, including positional projection and $slice, with distinct semantics.

For example, $slice can return only a portion of an array in the result:

db.posts.find(
  {},
  { title: 1, comments: { $slice: 5 } }
);

This returns up to five array elements in the projected comments field. It does not limit how many documents the query returns.

Projection is not authorization

Projection can help reduce data returned by a query, but sensitive fields should also be protected through authorization and careful API response design. Do not assume that selecting fields at the database layer alone implements every application security requirement.

Include and exclude fields
db.projectionDemo.insertOne({
  name: "Arun",
  email: "arun@example.com",
  passwordHash: "example-hash",
  address: {
    city: "Chennai",
    country: "India"
  },
  skills: ["Node.js", "MongoDB", "React"]
});

// Include selected fields and exclude _id.
db.projectionDemo.find(
  {},
  { name: 1, email: 1, _id: 0 }
);

// Return all fields except passwordHash.
db.projectionDemo.find(
  {},
  { passwordHash: 0 }
);

// Include a nested field and a subset of an array.
db.projectionDemo.find(
  {},
  { name: 1, "address.city": 1, skills: { $slice: 2 }, _id: 0 }
);
💡 The examples demonstrate inclusion projection, exclusion projection, nested-field projection, and array slicing. The inclusion and exclusion modes should not be mixed arbitrarily. Projection controls returned fields, not the number of documents returned.
Key Takeaways
  • Projection selects which fields are returned.
  • The _id field is included by default unless excluded.
  • Inclusion and exclusion projections generally cannot be mixed, apart from excluding _id.
  • Nested fields can be projected using dot notation.
  • Projection reduces returned data but is not a complete authorization strategy.
Knowledge Check

What does `{ name: 1, email: 1, _id: 0 }` do in a find projection?

Updating Documents with updateOne() and Update Operators

MongoDB update operations can modify selected fields without replacing an entire document. Modifier operators define how the selected document changes.

Basic update syntax

db.collection.updateOne(
  filter,
  updateDocument,
  options
)

The filter identifies the target document. The update document describes the modifications.

$set

$set creates a field if it does not exist or changes its value if it does.

db.users.updateOne(
  { name: "Arun" },
  { $set: { age: 23 } }
);

Other fields remain unchanged.

$unset

$unset removes a field from the document.

db.users.updateOne(
  { name: "Arun" },
  { $unset: { temporaryField: "" } }
);

The value supplied to $unset does not determine the resulting field value; the field is removed.

$inc

$inc increases or decreases a numeric field.

db.products.updateOne(
  { sku: "KEYBOARD-01" },
  { $inc: { stock: 5 } }
);

A negative increment decreases the field. If the field does not exist, $inc creates it with the specified value. If the field exists with a nonnumeric value, the update fails.

$mul

$mul multiplies a numeric field by a value.

db.products.updateOne(
  { sku: "MOUSE-01" },
  { $mul: { price: 1.1 } }
);

This operation changes the numeric value, but floating-point precision and the chosen numeric BSON type remain important.

$rename

$rename changes a field's name.

db.users.updateOne(
  { _id: ObjectId("65a000000000000000000001") },
  { $rename: { fullname: "name" } }
);

Renaming a field affects schema expectations, queries, and application code. Plan migrations carefully if multiple application versions are running.

$min and $max

$min updates a field only if the specified value is less than the current value under the applicable comparison rules. $max updates it only if the specified value is greater.

These operators can be useful for maintaining high-water marks or lowest observed values, but their behavior depends on type and comparison semantics.

$currentDate

$currentDate sets a field to the current date or a BSON Timestamp, depending on the requested type.

db.users.updateOne(
  { name: "Arun" },
  { $currentDate: { updatedAt: true } }
);

For application audit fields, BSON Date is usually the intended choice.

Multiple operators in one update

db.products.updateOne(
  { sku: "KEYBOARD-01" },
  {
    $set: { active: true },
    $inc: { stock: -1 },
    $currentDate: { updatedAt: true }
  }
);

The update applies all specified modifications atomically to the matched document. Ensure the filter identifies the correct target and that the resulting state satisfies your business rules.

Apply multiple update operators
db.updateDemo.insertOne({
  sku: "KEYBOARD-01",
  name: "Keyboard",
  stock: 10,
  active: false,
  obsoleteField: "remove-me"
});

db.updateDemo.updateOne(
  { sku: "KEYBOARD-01" },
  {
    $set: { active: true },
    $inc: { stock: 5 },
    $unset: { obsoleteField: "" },
    $currentDate: { updatedAt: true }
  }
);

db.updateDemo.findOne({ sku: "KEYBOARD-01" });
💡 The update enables the product, increments stock, removes an obsolete field, and records an update time. All changes are applied atomically to the matched document.
Key Takeaways
  • $set creates or modifies selected fields.
  • $unset removes fields.
  • $inc and $mul modify numeric values.
  • $rename changes field names and can affect application compatibility.
  • Multiple update operators can be applied atomically to one document.
Knowledge Check

What happens when `$inc` is applied to a field that does not exist?

Updating Multiple Documents with updateMany()

The updateMany() method applies an update to every document matching the supplied filter.

Basic example

db.products.updateMany(
  { category: "electronics" },
  { $set: { taxable: true } }
);

Every matching document receives the specified update.

Update all documents deliberately

An empty filter matches all documents:

db.products.updateMany(
  {},
  { $set: { reviewed: true } }
);

This is a valid operation, but it can modify the entire collection. Always verify that this is the intended behavior before running a broad update.

Update nested fields

db.users.updateMany(
  { "address.country": "India" },
  { $set: { "address.region": "South Asia" } }
);

Dot notation updates the nested region field without replacing the entire address document.

Increment matching documents

db.products.updateMany(
  { category: "books" },
  { $inc: { stock: 10 } }
);

Every matching document receives the increment.

Understand the result

An acknowledged update result typically provides:

  • matchedCount: the number of documents matching the filter.
  • modifiedCount: the number of documents actually modified.
  • upsertedCount: the number of documents inserted through upsert behavior, if enabled.
  • upsertedId: the identifier of an upserted document when applicable.

A matched document does not necessarily count as modified. If the update produces no change, matchedCount may exceed modifiedCount.

Multi-document atomicity

updateMany() is not a single atomic operation across every matching document. Other operations may observe partial progress, and an error can occur after some documents have been modified.

If the business requirement demands that several document changes succeed or fail together, evaluate a transaction supported by your deployment and workload. Transactions introduce additional costs and should not be used without understanding the consistency requirement.

Safe operational workflow

  1. Run a find query with the intended filter.
  2. Inspect representative matching documents.
  3. Estimate how many documents will be affected.
  4. Run the update in a suitable environment.
  5. Inspect matchedCount and modifiedCount.
  6. Verify the resulting documents.
An empty filter can update the entire collection

Before running updateMany({}, ...), verify that modifying every document is intentional. Test broad migrations in a non-production environment and use an appropriate recovery plan.

Update a group of documents
db.updateManyDemo.insertMany([
  { name: "Keyboard", category: "electronics", stock: 10 },
  { name: "Monitor", category: "electronics", stock: 5 },
  { name: "Book", category: "books", stock: 20 }
]);

// Inspect targets before updating.
db.updateManyDemo.find({ category: "electronics" });

const result = db.updateManyDemo.updateMany(
  { category: "electronics" },
  { $set: { taxable: true } }
);

printjson({
  matchedCount: result.matchedCount,
  modifiedCount: result.modifiedCount
});

db.updateManyDemo.find({ category: "electronics" });
💡 The operation updates all electronics documents. The result distinguishes documents that matched from documents that actually changed. Inspect the filter before running a broad update, particularly in production.
Key Takeaways
  • updateMany() modifies all documents matching the filter.
  • An empty filter matches all documents in the collection.
  • matchedCount and modifiedCount measure different things.
  • A multi-document update is not atomic as a whole.
  • Inspect and validate the target set before broad updates.
Knowledge Check

Why can matchedCount be greater than modifiedCount?

Updating Array Fields and Array Elements

MongoDB provides update operators for adding, removing, and modifying array elements.

$push

$push appends a value to an array.

db.users.updateOne(
  { name: "Arun" },
  { $push: { skills: "MongoDB" } }
);

If the array does not exist, $push creates it. If the field exists but is not an array, the update fails.

$addToSet

$addToSet adds a value only if an equal value is not already present.

db.users.updateOne(
  { name: "Arun" },
  { $addToSet: { skills: "MongoDB" } }
);

For arrays of embedded documents, equality is based on document value comparison, including field order. $addToSet does not enforce uniqueness by a selected nested key such as skillId.

$each

$each adds multiple values with operators such as $push and $addToSet.

db.users.updateOne(
  { name: "Arun" },
  {
    $addToSet: {
      skills: {
        $each: ["React", "Node.js", "MongoDB"]
      }
    }
  }
);

$pull

$pull removes all array elements that match the supplied condition.

db.users.updateOne(
  { name: "Arun" },
  { $pull: { skills: "LegacySkill" } }
);

For an array of embedded documents, you can match by nested field values.

$pop

$pop removes one element from either end of an array.

// Remove the last element.
db.users.updateOne(
  { name: "Arun" },
  { $pop: { skills: 1 } }
);

// Remove the first element.
db.users.updateOne(
  { name: "Arun" },
  { $pop: { skills: -1 } }
);

$pullAll

$pullAll removes all array elements equal to any of the specified values.

db.users.updateOne(
  { name: "Arun" },
  { $pullAll: { skills: ["LegacySkill", "DeprecatedSkill"] } }
);

Positional update operator

The positional $ operator updates the first array element matched by the query condition in supported update patterns.

For example, when the query identifies a student and a matching score element, the positional operator can target that element.

db.students.updateOne(
  { _id: 1, "scores.subject": "Math" },
  { $set: { "scores.$.score": 95 } }
);

The positional operator has restrictions and should be used only when the query correctly identifies the intended array element.

Array filters

Array filters allow updates to selected elements using identifiers in the update path.

db.students.updateOne(
  { _id: 1 },
  { $set: { "scores.$[item].passed": true } },
  { arrayFilters: [{ "item.score": { $gte: 40 } }] }
);

This updates every matching array element in the selected document. The update path identifier and array filter identifier must correspond.

Positional all operator

The $[] operator targets all elements of an array.

db.students.updateOne(
  { _id: 1 },
  { $inc: { "scores.$[].attempts": 1 } }
);

This increments attempts for every element in the selected document's scores array, assuming the path and element structures are valid.

Manipulate array values
db.arrayUpdateDemo.insertOne({
  name: "Arun",
  skills: ["JavaScript", "React"],
  scores: [
    { subject: "Math", score: 35, passed: false },
    { subject: "Science", score: 55, passed: true }
  ]
});

// Add a unique skill.
db.arrayUpdateDemo.updateOne(
  { name: "Arun" },
  { $addToSet: { skills: "MongoDB" } }
);

// Add multiple unique skills.
db.arrayUpdateDemo.updateOne(
  { name: "Arun" },
  {
    $addToSet: {
      skills: { $each: ["Node.js", "MongoDB"] }
    }
  }
);

// Update the Math score.
db.arrayUpdateDemo.updateOne(
  { name: "Arun", "scores.subject": "Math" },
  { $set: { "scores.$.score": 45, "scores.$.passed": true } }
);

// Inspect the result.
db.arrayUpdateDemo.findOne({ name: "Arun" });
💡 The example demonstrates adding unique array values and updating the first matching embedded score element. Array updates should use filters that identify the intended document and array element.
Key Takeaways
  • $push appends values, while $addToSet avoids adding an equal existing value.
  • $pull removes matching array elements.
  • $pop removes one element from an end of an array.
  • The positional $ operator targets a matched array element.
  • Array filters can target selected elements using explicit identifiers.
Knowledge Check

Which update operator is designed to add a value only if an equal value is not already present in an array?