Unique Indexes

A unique index in MongoDB guarantees that no two documents in a collection can have the same value for the indexed field (or field combination) — the same guarantee a UNIQUE constraint gives you in a relational database. Without it, MongoDB will happily let you insert ten users with the identical email address, because by default nothing stops duplicate values. Unique indexes turn "this field should never repeat" from a convention your application hopes to follow into a rule the database enforces.

Overview / How it works

Every index in MongoDB, unique or not, is a separate tree-like data structure that maps field values to the location of the documents that contain them. A normal index speeds up lookups but allows duplicate keys in that structure. A unique index adds one extra rule at write time: before MongoDB commits an insert or update, it checks whether the new key value already exists anywhere in the index. If it does, the write is rejected with a duplicate key error (error code 11000), and the document is not inserted or modified.

This check happens at the storage layer during the write itself, not as an application-level validation step, which is exactly why it stays reliable under concurrent writes: two application servers racing to insert the same email address cannot both succeed, because the index — not your Node.js code — makes the final call. This is also why a compound unique index enforces uniqueness of the combination of fields, not each field independently — { customerId: 1, orderNumber: 1 } as a unique compound index allows the same orderNumber to repeat across different customers, but not twice for the same customer.

A subtlety that trips up almost everyone eventually: MongoDB treats a missing field as the value null for indexing purposes. If a unique index sits on phone and two documents simply don't have a phone field, both are indexed as phone: null — and the second insert fails, because as far as the unique index is concerned, two nulls are a duplicate. Fixing this correctly requires either a partial index (indexing only documents that actually have the field) or a sparse index, both covered below.

The _id field is a good mental model: every collection already has a unique index on _id, created automatically when the collection is first used, and it can never be dropped. Every other unique index you add just extends that same enforcement mechanism to whatever field or fields you choose.

Syntax

db.collection.createIndex(
  { field: 1 },
  { unique: true }
);
Parameter Description
{ field: 1 } The key pattern — the field(s) to index and their sort direction (1 ascending, -1 descending; direction doesn't affect uniqueness itself but matters if the index also serves sorts).
unique Boolean. When true, rejects any insert or update that would create a second document with the same key value(s).
partialFilterExpression Restricts the unique constraint to only documents matching this filter — the standard way to let multiple documents omit the field, or not meet some condition, without colliding.
sparse Excludes documents missing the indexed field from the index entirely. An older alternative to partialFilterExpression for the "allow multiple missing" case; partial indexes are generally preferred now since they're more flexible.
name Optional custom index name, instead of the auto-generated field_1 style name.
collation Optional language-aware string comparison rules, which also change what counts as a "duplicate" for case- or accent-insensitive uniqueness.

Examples

Start with the simplest case: enforcing that every user has a distinct email address.

db.users.createIndex({ email: 1 }, { unique: true });

Output:

"email_1"

createIndex() returns the name of the index it just built. From this point on, MongoDB refuses any write that would duplicate an email value.

Now try to violate it:

db.users.insertOne({ name: "Ava Chen", email: "ava@example.com" });
db.users.insertOne({ name: "Ben Ortiz", email: "ava@example.com" });

Output:

{ acknowledged: true, insertedId: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1") }
MongoServerError: E11000 duplicate key error collection: shop.users index: email_1 dup key: { email: "ava@example.com" }

The first insert succeeds and the index now holds one entry for ava@example.com. The second insert is rejected outright — no insertedId is generated, no document is written, and mongosh prints an error carrying code 11000. Application code talking to this collection should catch that error code specifically rather than assuming every failed insert means something is broken.

Unique indexes aren't limited to a single field. A compound unique index enforces uniqueness of the combination:

db.orders.createIndex(
  { customerId: 1, orderNumber: 1 },
  { unique: true }
);

Output:

"customerId_1_orderNumber_1"

Now customer A and customer B can each have an orderNumber of 1 — that's two different combinations — but customer A cannot have two documents that both have orderNumber: 1.

Finally, the fix for the "missing field" problem described above — allow any number of documents to omit phone, but forbid two documents from sharing the same non-missing phone value:

db.users.createIndex(
  { phone: 1 },
  { unique: true, partialFilterExpression: { phone: { $exists: true } } }
);

Output:

"phone_1"

Documents without a phone field are simply left out of the index, so they never collide with each other. Only documents that do have phone get checked for duplicates.

How it works step by step

When you run createIndex() with unique: true on a collection that already has data, MongoDB scans every existing document and builds the index entry by entry. If it finds two documents whose key values already collide, the build fails immediately with a duplicate key error and the index is not created — existing bad data has to be cleaned up first, since a unique index can never be built on top of duplicates that already exist.

Once the index exists, every subsequent insertOne, insertMany, updateOne, or updateMany that would change an indexed field is checked against it as part of the write path, before the storage engine commits the change. On a replica set this check happens on the primary; the resulting write (or the absence of one) then replicates to secondaries, so the constraint stays consistent cluster-wide, not just locally. With insertMany(), MongoDB by default stops at the first duplicate-key failure (ordered inserts); passing { ordered: false } lets it skip the failing document and keep going, reporting all failures together at the end.

Common Mistakes

Mistake 1: Adding a unique index to a collection that already has duplicates.

db.users.createIndex({ username: 1 }, { unique: true });

Output:

MongoServerError: E11000 duplicate key error collection: shop.users index: username_1 dup key: { username: "jsmith" }

This fails because two or more existing documents already share a username. Find and resolve the duplicates first:

db.users.aggregate([
  { $group: { _id: "$username", count: { $sum: 1 }, ids: { $push: "$_id" } } },
  { $match: { count: { $gt: 1 } } }
]);

Output:

[
  { _id: "jsmith", count: 2, ids: [ ObjectId("64f1a2..."), ObjectId("64f1b7...") ] }
]

Rename or merge the duplicates this reveals, then run createIndex() again.

Mistake 2: Assuming a unique index automatically allows unlimited documents with a missing field.

db.users.createIndex({ ssn: 1 }, { unique: true });
db.users.insertOne({ name: "Cara Lin" });
db.users.insertOne({ name: "Drew Patel" });

Output:

{ acknowledged: true, insertedId: ObjectId("64f1c1...") }
MongoServerError: E11000 duplicate key error collection: shop.users index: ssn_1 dup key: { ssn: null }

Both documents lack ssn, so both are indexed as ssn: null, and the second insert collides with the first. Use a partial (or sparse) index so documents without the field are excluded entirely:

db.users.createIndex(
  { ssn: 1 },
  { unique: true, partialFilterExpression: { ssn: { $exists: true } } }
);

Best Practices

  • Create unique indexes for any field your application logic already treats as one-to-one — email, username, SKU, order number — instead of relying only on an application-level check, which can't stop a race between two concurrent requests.
  • Use partialFilterExpression whenever the field can legitimately be absent on some documents, so those documents don't collide with each other as null.
  • Check for existing duplicates with an aggregation $group before adding a unique index to a populated collection, rather than discovering the problem from a failed createIndex() call.
  • Remember a unique index is also a normal index — it speeds up equality lookups on that field just like a non-unique index would, so you rarely pay a performance cost for adding one.
  • Catch duplicate key errors (error code 11000) explicitly in application code and turn them into a friendly "that value is already taken" message instead of surfacing a raw database error.
  • For case-insensitive uniqueness (so Ava@Example.com and ava@example.com are treated as the same email), add a collation with an appropriate strength rather than normalizing case only in application code.

Practice Exercises

  • Create a products collection and add a unique index on sku. Insert two products with different SKUs (should succeed), then insert a third with a SKU that already exists (should fail with an E11000 error).
  • On an employees collection, add a unique index on badgeNumber that still allows any number of contractors to have no badge number at all. Verify by inserting three documents with no badgeNumber field.
  • Design a compound unique index for a reviews collection so a given userId can only leave one review per productId, but different users can review the same product freely. Test it with a few inserts to confirm the constraint behaves as expected.

Summary

  • A unique index rejects any insert or update that would duplicate the indexed field's value, enforced at the storage layer during the write itself.
  • Missing fields are indexed as null, so multiple documents without the field will collide unless you use a partial or sparse index.
  • Compound unique indexes enforce uniqueness of the field combination, not each field independently.
  • Building a unique index on a collection with existing duplicates fails — clean up duplicates first, using an aggregation $group to find them.
  • A unique index doubles as a regular index, so it also speeds up queries and sorts on that field.