Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 008 / 100

MongoDB update operators: a hands-on guide for engineers

Change documents atomically.

Expona Admin7 min read

Why update operators matter

When you need to change a document in MongoDB, your instinct might be to fetch it, modify it in application code, and write it back. That pattern works, but it leaves you vulnerable to race conditions and wastes network round-trips. MongoDB update operators: a hands-on guide for engineers teaches you a better way. By sending atomic operations directly to the database, you eliminate entire classes of bugs and simplify your code.

Update operators let MongoDB handle mutations on the server side, guaranteeing isolation and consistency for that single document. In production systems—whether you're tracking likes on posts, incrementing cart quantities, or managing feature flags—this atomic guarantee becomes non-negotiable. Today we'll walk through the operators you'll use most often and show you how to build them into your data mutations from day one.

$set and $unset — Change or remove fields

The $set operator is the workhorse of updates. It replaces or creates a field without touching anything else in the document. This is safer than wholesale document replacement because you only modify what you intend to modify.

javascript

// Update a user's email and last_login timestamp
db.users.updateOne(
  { _id: ObjectId("507f1f77bcf86cd799439011") },
  { $set: { email: "new@example.com", last_login: new Date() } }
);

In a real e-commerce platform, $set is your go-to for profile updates, shipping address changes, and preference toggles. You send only the fields that changed, and MongoDB applies them atomically. The rest of the document—your nested arrays, embedded objects, audit logs—stays untouched.

Use $unset when you need to remove a field entirely, rather than setting it to null. This is especially useful for optional fields that may not apply to all documents in a collection.

javascript

// Remove a deprecated field from all documents
db.users.updateMany(
  { },
  { $unset: { legacy_subscription_id: "" } }
);

The value you provide to $unset is ignored; MongoDB removes the field regardless. In migrations, this pattern lets you clean up old schema fields without downtime or bulk rewrites.

$inc — Atomic counters without read-modify-write

Counting things at scale is where $inc shines. You want to increment a view counter, a like count, or an order total without fetching the document, incrementing in code, and saving it back. That three-step dance invites race conditions: two threads might both read 100, both increment to 101, and write 101 back—losing one increment.

With $inc, you send the operation to MongoDB and let the server handle it atomically.

javascript

// Increment view count by 1
db.posts.updateOne(
  { _id: postId },
  { $inc: { views: 1 } }
);

// Decrement inventory count by 5
db.products.updateOne(
  { _id: productId },
  { $inc: { stock: -5 } }
);

In a ride-hailing system, drivers and riders both accumulate ratings. Every time a trip ends, you'd increment the driver's trip count and update their average rating using $inc plus other operators, all in one update call. No fetch, no race condition, no stale data.

You can also increment multiple numeric fields in a single update. This is common when tracking metrics: increment both the view count and the click count on an ad impression, for example.

javascript

db.analytics.updateOne(
  { campaign_id: "spring_2024" },
  { $inc: { impressions: 1, clicks: 1, spend: 2.50 } }
);

Array operators — $push, $pull, and $addToSet

MongoDB treats arrays as first-class values. You can modify them atomically without replacing the entire array, which becomes critical when arrays grow large or are updated concurrently.

$push appends an element to an array. If the field doesn't exist, it creates it as an array with that element.

javascript

// Add a comment to a post
db.posts.updateOne(
  { _id: postId },
  { $push: { comments: { user_id: userId, text: "Great post!", created_at: new Date() } } }
);

$addToSet appends an element only if it doesn't already exist in the array. This is your tool for maintaining sets of unique values—tags, user IDs in an allow list, email addresses on a notification list.

javascript

// Add a user to a notification list (only once)
db.notificationLists.updateOne(
  { _id: listId },
  { $addToSet: { subscribers: userId } }
);

$pull removes an element from an array by value or condition. This is how you unlike a post or remove a follower.

javascript

// Remove a tag from a product
db.products.updateOne(
  { _id: productId },
  { $pull: { tags: "clearance" } }
);

Combining operators is powerful. In a social platform, when a user likes a post, you want to add their ID to the likes array and increment the like count in one operation. Here's the real-world pattern:

javascript

// Atomic like operation
db.posts.updateOne(
  { _id: postId },
  {
    $addToSet: { likes: userId },
    $inc: { likeCount: 1 }
  }
);

// And to unlike:
db.posts.updateOne(
  { _id: postId },
  {
    $pull: { likes: userId },
    $inc: { likeCount: -1 }
  }
);

This guarantees that the count and the array stay in sync. You're not doing two separate operations; it's all or nothing, atomically, on the server.

Upserts — Create the document if it's missing

An upsert combines "update if exists, insert if doesn't." You pass upsert: true to your update call, and MongoDB will create the document with the update operators applied if no match is found.

javascript

// Increment today's page view counter
// Creates the document if it doesn't exist
db.dailyStats.updateOne(
  { date: "2024-01-15", page: "/products" },
  { $inc: { views: 1 } },
  { upsert: true }
);

Upserts are invaluable for counters, summaries, and aggregations. In an analytics pipeline, you might process millions of events per day. Rather than inserting a new document for each unique (date, page) pair, you upsert and increment. The collection grows linearly with the number of unique pages, not the number of events.

When an upsert creates a new document, it uses the filter to set fields and the update operators to set others. This can sometimes surprise you, so be explicit:

javascript

// Upsert with explicit field initialization
db.userSessions.updateOne(
  { user_id: userId, session_id: sessionId },
  {
    $set: { created_at: new Date(), status: "active" },
    $inc: { request_count: 1 }
  },
  { upsert: true }
);

If the document exists, we increment the request count and set status. If it doesn't exist, MongoDB creates it with all three fields (user_id, session_id, created_at, status, and request_count: 1).

Common mistakes

Forgetting that $set replaces, not merges nested objects. If you do { $set: { address: { city: "New York" } } }, you lose the street and zip code. Use dot notation to update nested fields: { $set: { "address.city": "New York" } }.

Mixing update operators incorrectly. You can't use $set and $inc on the same field in one update. Pick one. Also, don't nest operators: { $set: { $inc: { count: 1 } } } won't work.

Assuming $addToSet compares objects by reference. When adding objects to an array with $addToSet, MongoDB checks for equality. If two comment objects have the same fields but were created separately, $addToSet will add both. Use a unique identifier (like comment_id) if you need true deduplication.

Not indexing filter fields in updates. If you update documents by a field that isn't indexed, each update triggers a full collection scan. Index the fields in your update filter the same way you'd index a query.

Key takeaways

  • Use $set and $unset to modify individual fields atomically without touching the rest of the document.
  • $inc eliminates read-modify-write races for counters; always prefer it over fetching and incrementing in application code.
  • Array operators ($push, $pull, $addToSet) let you modify arrays as first-class values; combine them with $inc for operations like liking posts.
  • Upserts (upsert: true) reduce code paths and network calls for insert-or-update patterns; use them for analytics and rolling counters.
  • Index your update filter fields and test your operator combinations carefully to avoid losing data in nested objects.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles