Upsert: Update or Insert

An upsert combines update behavior with conditional insertion. When an update operation has upsert: true, MongoDB inserts a document if no document matches the filter.

Example: Upsert a settings document

db.userSettings.updateOne(
  { userId: "user-1001" },
  {
    $set: {
      theme: "dark",
      language: "en"
    }
  },
  { upsert: true }
);

If a matching document exists, its selected fields are updated. Otherwise, MongoDB creates a document based on the filter and update operation.

$setOnInsert

$setOnInsert applies fields only when the upsert inserts a new document.

db.userSettings.updateOne(
  { userId: "user-1001" },
  {
    $set: { theme: "dark" },
    $setOnInsert: {
      createdAt: new Date(),
      language: "en"
    }
  },
  { upsert: true }
);

If the document already exists, theme is updated but the $setOnInsert fields are not applied. If no document matches, those fields are included in the inserted document.

Upsert and filter fields

When an upsert inserts a document, MongoDB derives parts of the inserted document from the equality conditions in the filter, together with the update modifications and operation-specific behavior.

Do not assume that arbitrary range conditions or complex logical expressions will be converted into a useful document structure. Design upsert filters around stable identifying keys.

The concurrency problem

Suppose two requests attempt to upsert the same logical record at approximately the same time. Without an appropriate unique index, the application may create multiple documents representing the same logical entity if the filters and execution allow it.

A unique index on the logical key is often essential:

db.userSettings.createIndex(
  { userId: 1 },
  { unique: true }
);

The index enforces uniqueness of userId values across documents, subject to the index's handling of missing or null values and any partial-index configuration.

Applications should still handle duplicate-key errors and define how concurrent requests are retried or reconciled.

Idempotent application design

An upsert keyed by a stable unique identifier can help implement idempotent write behavior. However, not every update is idempotent. For example, $inc changes the value each time the operation executes.

If an application retries an increment after an ambiguous network failure, the increment may be applied more than once unless the workflow provides additional deduplication or transactional safeguards.

Use a unique key for logical identity

An upsert filter identifies the document to update or create. When the application requires one document per logical key, enforce that rule with an appropriate unique index and handle duplicate-key errors.

Create or update a user settings document
db.upsertDemo.createIndex({ userId: 1 }, { unique: true });

db.upsertDemo.updateOne(
  { userId: "user-1001" },
  {
    $set: {
      theme: "dark"
    },
    $setOnInsert: {
      language: "en",
      createdAt: new Date()
    }
  },
  { upsert: true }
);

db.upsertDemo.findOne({ userId: "user-1001" });
💡 The first operation creates a unique index on the logical user identifier. The upsert updates the theme when the document exists or inserts a new settings document with the initialization fields when it does not. The unique index helps prevent duplicate settings records.
Key Takeaways
  • An upsert updates a match or inserts a document when no match exists.
  • $setOnInsert applies fields only when the upsert inserts a document.
  • Stable identifying filters are important for predictable upsert behavior.
  • A unique index helps enforce one document per logical key.
  • An increment is not idempotent simply because it is performed through an upsert.
Knowledge Check

When does `$setOnInsert` apply its fields?