Upserts & Bulk Writes
Some writes do not know in advance whether the document exists. "Add one to today's view count for this product" should create the counter the first time and increase it afterwards. "Save this customer's login time" should create a profile for a brand-new user. MongoDB handles this with an upsert: an update that inserts a document when nothing matches.
In this lesson you will write upserts, control which fields are set only on insert, build a counter for order numbers, and send many different writes in one request with bulkWrite(). Start with freshly loaded data and use shop.
Upsert: update or insert
Add { upsert: true } as the third argument of updateOne(), updateMany() or replaceOne(). If a document matches, it is updated as usual. If none matches, MongoDB builds a new document from the filter's equality fields, applies the update to it and inserts it.
The shop counts product page views per day. The first view of the earbuds today creates the counter:
db.productViews.updateOne(
{ productId: 105, day: '2026-10-30' },
{ $inc: { views: 1 } },
{ upsert: true }
){
acknowledged: true,
insertedId: ObjectId('6ae43a7cc3f5e8a91d2b7a07'),
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
}Nothing matched, so upsertedCount is 1 and insertedId holds the _id of the new document. Run the exact same command a second time and the result shows matchedCount: 1, modifiedCount: 1, upsertedCount: 0, because the counter now exists. Look at the document:
db.productViews.findOne({ productId: 105 }){
_id: ObjectId('6ae43a7cc3f5e8a91d2b7a07'),
productId: 105,
day: '2026-10-30',
views: 2
}productId and day came from the filter, and views came from $inc, which treats a missing field as 0. One command handles both the first view and every later one, with no "does it exist?" check in your code.
$setOnInsert: fields for new documents only
Sometimes a field should be written only when the document is created. $setOnInsert does exactly that; on an update of an existing document it is ignored. A customer logs in with an email address: if she is new, create her profile; either way, record the login time:
db.customers.updateOne(
{ email: 'neha@example.com' },
{
$set: { lastLogin: ISODate('2026-10-30T08:00:00Z') },
$setOnInsert: { _id: 9, name: 'Neha Joshi', city: 'Lucknow', joined: ISODate('2026-10-30'), premium: false }
},
{ upsert: true }
){
acknowledged: true,
insertedId: 9,
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
}Neha was new, so all the fields were written. If she logs in again tomorrow with a new lastLogin, only that field changes; her joined date and name stay as they were. A field may not appear in both $set and $setOnInsert in the same update.
A counter with findOneAndUpdate
Order numbers such as 1013, 1014, 1015 need a counter that never hands out the same number twice, even under heavy traffic. Keep one document per counter and increase it atomically, asking for the value after the change:
db.counters.findOneAndUpdate(
{ _id: 'orderNo' },
{ $inc: { seq: 1 } },
{ upsert: true, returnDocument: 'after' }
){ _id: 'orderNo', seq: 1 }The first call creates the counter with seq: 1; each later call returns 2, 3 and so on. To continue from the shop's existing orders, start the counter at 1012 once with db.counters.insertOne({ _id: 'orderNo', seq: 1012 }) before the first call. You will use this pattern in Project: Online Shop Backend. Most applications are happy with ObjectIds, so use counters only when people need short sequential numbers, such as invoice numbers.
bulkWrite(): many writes in one request
bulkWrite() sends a list of different write operations on one collection to the server in a single round trip. Each element of the list names its operation: insertOne, updateOne, updateMany, replaceOne, deleteOne or deleteMany. A supplier's weekly price file might produce this batch:
db.products.bulkWrite([
{ insertOne: { document: { _id: 113, name: 'Phone Tripod', category: 'Mobile', price: 699, stock: 40, rating: 4.0, tags: ['mobile', 'video'] } } },
{ updateOne: { filter: { _id: 106 }, update: { $set: { stock: 20 } } } },
{ updateOne: { filter: { _id: 105 }, update: { $set: { price: 2299 } } } },
{ updateOne: {
filter: { _id: 114 },
update: { $setOnInsert: { name: 'Laptop Sleeve', category: 'Accessories', price: 799, stock: 35, rating: 4.2, tags: ['laptop'] } },
upsert: true
} }
], { ordered: false }){
acknowledged: true,
insertedCount: 1,
insertedIds: { '0': 113 },
matchedCount: 2,
modifiedCount: 2,
deletedCount: 0,
upsertedCount: 1,
upsertedIds: { '3': 114 }
}The result totals every kind of write. insertedIds and upsertedIds are keyed by the position of the operation in your list: operation 0 inserted product 113 and operation 3 upserted product 114.
As with insertMany(), bulk writes are ordered by default and stop at the first error; { ordered: false } lets the remaining operations run and lets the server execute them in any order. A bulk write is not a transaction: if operation 3 fails, operations 0 to 2 stay done. When several writes must succeed or fail together, use Transactions.
When you have finished, reload the shop data with load('shop-data.js').
Common mistakes
- Upserting without a unique index. Concurrent upserts can create duplicates. Index the filter fields uniquely.
- Putting changing values in the filter.
{ email: e, lastLogin: t }as an upsert filter never matches yesterday's document, so you get a new document every day. Filter only on what identifies the document. - Expecting
$setvalues only on insert.$setapplies on every call; use$setOnInsertfor creation-only fields such asjoined. - Treating
bulkWrite()as all-or-nothing. Earlier operations are not undone when a later one fails.
Key points
{ upsert: true }updates a matching document or inserts a new one built from the filter and the update.- The result's
upsertedCountandinsertedIdtell you when an insert happened. $setOnInsertwrites fields only when the upsert creates a document.findOneAndUpdate()with$inc,upsertandreturnDocument: 'after'makes a safe sequence counter.bulkWrite()sends mixed inserts, updates, replaces and deletes on one collection in one round trip; it is not a transaction.- MongoDB 8.0's
bulkWritecommand can write to several collections in one request.
Exercise
Customers can save products to a wish list. Write one upsert that adds product 107 to customer 4's document in a wishlists collection (using customerId as the identifying field), creating the wish list with a createdAt date if it does not exist and never storing the same product twice. Run it twice and predict both results.
Show solution
db.wishlists.updateOne(
{ customerId: 4 },
{ $addToSet: { productIds: 107 }, $setOnInsert: { createdAt: ISODate('2026-10-30') } },
{ upsert: true }
)The first run inserts a new wish list (upsertedCount: 1, insertedId is a new ObjectId) with productIds: [107]. The second run matches it (matchedCount: 1) but $addToSet finds 107 already there, so modifiedCount is 0, and $setOnInsert is ignored. A unique index on customerId would protect against two simultaneous first visits.
Quick Quiz
1. What does { upsert: true } do when no document matches the filter?
An upsert inserts when nothing matches, combining the filter's equality conditions with the update operators.
2. When are $setOnInsert fields written?
$setOnInsert is ignored when the update modifies an existing document.
3. An ordered bulkWrite() of four operations fails at operation 2. What is true?
Ordered bulk writes stop at the first error and do not undo earlier operations.
Learn MongoDB with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
