Key Takeaways
- A MongoDB unique index prevents different documents from sharing the same indexed key.
- A single-field unique index constrains one field; a compound unique index constrains the complete field combination.
- Regular, sparse, and partial unique indexes handle optional fields differently.
- Resolve conflicting data before building the index, verify deployment, and handle duplicate-key errors in your application.
A MongoDB unique index enforces uniqueness in the database, either on one field or on a combination of fields. Once the index is built, MongoDB rejects inserts and updates that would create a conflicting index key. Existing conflicts must be resolved before the index can be created successfully.
How does a MongoDB unique index work?
An index key is the value, or combination of values, that MongoDB records in an index. Uniqueness applies to that key—not automatically to every field participating in it.
MongoDB enforces the constraint as writes modify index entries, including concurrent writes. An application-side lookup followed by an insert isn't a substitute: two requests can both check before either writes, but the unique index still rejects a conflicting write. This is database enforcement, not a separate before-and-after validation pass.
MongoDB normally creates a unique index on _id when you create an ordinary collection. Sharded collections require an important qualification: when _id isn't the shard key, that index enforces uniqueness per shard, not across the entire cluster.
How to create a unique index in MongoDB
Run these examples in mongosh, connected to a test database. The examples assume scalar email and user fields and no custom collation, meaning no customized string-comparison rules.
Create a single-field unique index
db.people.createIndex({email: 1}, {unique: true});The 1 selects ascending index order; unique: true adds the uniqueness constraint. Two documents cannot share the same indexed email value. The regular index also constrains missing and null values, which we'll cover below. For the general syntax, see our MongoDB create index tutorial.
After creation succeeds, inspect the deployed indexes:
db.people.getIndexes();The result describes the collection's indexes, including their key patterns and options. Find the entry with key: { email: 1 } and unique: true; don't assume an application declaration created it. Our guide to listing MongoDB indexes explains inspection in more detail.
Recognize a duplicate-key write error
With the email index in place, this example inserts one person and handles the conflicting second write:
db.people.insertOne({ email: "ada@example.com", user: "ada" });
try {
db.people.insertOne({ email: "ada@example.com", user: "another-user" });
} catch (error) {
if (error.code === 11000) {
print("A unique index rejected this write.");
} else {
throw error;
}
}Duplicate-key errors use MongoDB error code 11000, commonly displayed as E11000 duplicate key error. Check the numeric code rather than matching an entire error message; the conflicting index might be _id or another unique index, not necessarily email.
Create a MongoDB unique compound index
db.people.createIndex({email: 1, user: 1}, {unique: true});This makes the complete (email, user) pair unique. Neither email nor user becomes individually unique. Assuming (ada@example.com, ada) already exists, the following table shows the difference between the two designs.
| Candidate pair | Unique email only | Unique email + user only |
|---|---|---|
| (ada@example.com, grace) | Rejected | Allowed |
| (grace@example.com, ada) | Allowed | Allowed |
| (ada@example.com, ada) | Rejected | Rejected |
If both fields must be independently unique, create separate unique indexes. Use our compound index tutorial for the broader indexing pattern.
How unique indexes handle null and missing fields
A regular, non-sparse single-field unique index stores a null index key for a document whose indexed field is missing or explicitly null. It therefore allows only one document across those two cases combined—not one missing document plus one null document.
For a single scalar email field, the options compare as follows. Sparse indexes exclude missing fields but still index explicit nulls; partial indexes constrain only documents matching their filter.
| Unique index design | Missing email | Explicit email: null | Constraint |
|---|---|---|---|
| Regular, non-sparse | Indexed as null | Indexed as null | Only one null-key document total |
| Sparse | Excluded; multiple allowed | Indexed; only one allowed | Unique among documents containing the field |
| Partial: email has string type | Excluded; multiple allowed | Excluded; multiple allowed | Unique among matching documents |
Use a partial unique index for optional email addresses
If email is optional, a partial unique index usually expresses the requirement more clearly:
db.people.createIndex(
{ email: 1 },
{
name: "unique_email_strings",
unique: true,
partialFilterExpression: { email: { $type: "string" } }
}
);For scalar fields, this indexes string emails while excluding missing values, nulls, and non-string values. Duplicate indexed strings are rejected; documents outside the filter aren't constrained by this index. An empty string is still a string, so it isn't excluded.
This isn't email-format or field-type validation. In particular, the query form of $type can also match arrays containing a string. If email must be a scalar string, enforce that shape separately.
Compound indexes constrain the complete null-containing key
The compound rule still applies when components are missing or null. For example, keys (null, "ada") and (null, "grace") differ and can coexist; a repeated (null, "ada") conflicts. A regular unique index on { email: 1, user: 1 } permits only one document with the complete key (null, null). These outcomes follow from applying uniqueness to the entire indexed pair.
Find duplicates before creating the unique index
For scalar email values, use an aggregation pipeline to report conflicting groups:
db.people.aggregate([
{
$group: {
_id: { $ifNull: ["$email", null] },
count: { $sum: 1 }
}
},
{ $match: { count: { $gt: 1 } } }
]);$group counts documents sharing a key. $ifNull deliberately combines missing and null emails, matching the regular single-field index's treatment of those cases.
For the compound index, group by the intended pair instead:
db.people.aggregate([
{
$group: {
_id: {
email: { $ifNull: ["$email", null] },
user: { $ifNull: ["$user", null] }
},
count: { $sum: 1 }
}
},
{ $match: { count: { $gt: 1 } } }
]);For the partial email index, add { $match: { email: { $type: "string" } } } before $group. Match the intended index's filter and string-comparison rules when auditing data. These pipelines assume scalar fields; they aren't multikey duplicate detectors.
Resolve conflicts, then build and verify
- Inspect each duplicate group. Choose a canonical record, merge useful data, and repair references before deleting anything. If the records are legitimately different, reconsider the uniqueness rule instead.
- Control conflicting writes. Pause writers that can introduce duplicates during cleanup and the index build.
- Create the chosen index. A failed build doesn't deduplicate or repair the collection.
- Verify success and inspect the definition. Only then should your application rely on the constraint.
Duplicates can still be written while a unique index is being built. If violations remain when the build finishes, MongoDB fails the build. A clean preflight query alone doesn't eliminate that deployment race.
Already have a non-unique index? Don't assume calling createIndex() with changed options converts it. MongoDB documents a separate conversion workflow using collMod; plan that migration explicitly.
Mongoose unique: true is not a validator
In Mongoose, unique: true declares a MongoDB index; it isn't a Mongoose validator. Automatic index creation depends on settings such as autoIndex, and the database index must actually exist before it can enforce anything.
This application fragment assumes an open Mongoose connection and that you've already deployed the regular unique email index:
import mongoose from "mongoose";
// ESM fragment: run after your application connects to MongoDB.
const personSchema = new mongoose.Schema(
{ email: { type: String, unique: true } },
{ autoIndex: false }
);
const Person = mongoose.model("Person", personSchema, "people");
try {
await Person.create({ email: "ada@example.com" });
} catch (error) {
if (error?.code === 11000) {
console.error("A unique index rejected this write.");
} else {
throw error;
}
}Here, autoIndex: false leaves index deployment to your migration process. A duplicate write produces a database duplicate-key error, not a Mongoose validation error. Map that failure to an appropriate application response rather than exposing raw database error details.
Arrays and sharded collections need extra care
Unique multikey indexes constrain different documents
An index on an array-valued field becomes a multikey index, which indexes array elements. With a unique index on aliases, one document can contain ["ada", "ada"]; another document cannot also claim the indexed alias "ada". Repeated values inside one document don't by themselves violate uniqueness.
Sharding changes which unique indexes are permitted
For a ranged sharded collection, a unique index generally must be the shard-key index or have the shard key as its leading prefix. For example, with shard key { tenantId: 1 }, a unique { tenantId: 1, email: 1 } index constrains the tenant/email pair—not email globally. Hashed indexes cannot be unique.
The default _id index is the special case: if _id isn't the shard key, your application must ensure cluster-wide _id uniqueness.
Production Checklist
- Write down the exact business key: email, email/user, or a tenant-scoped combination.
- Choose how missing values, nulls, field types, and string comparison should behave before cleaning data.
- Test duplicate inserts and updates, including competing requests, against the deployed index.
- Schedule and monitor large builds: index creation under heavy write load can reduce write performance. Duplicate-audit aggregations can also consume substantial memory. See our MongoDB index performance guide for broader trade-offs.
FAQ
How do I create a unique index in MongoDB?
Run db.people.createIndex({email: 1}, {unique: true}) in mongosh to create a single-field unique email index. Resolve existing conflicting keys first, then verify the index with db.people.getIndexes().
Can a MongoDB unique index contain multiple null or missing values?
A regular single-field unique index permits only one document whose indexed field is null or missing, combined. A sparse index excludes missing fields but includes nulls; a partial index can exclude both. Compound indexes enforce uniqueness on the complete key combination.
How do I create a unique index on multiple fields?
Run db.people.createIndex({email: 1, user: 1}, {unique: true}). This makes the email/user combination unique, but neither field is individually unique unless you also create separate unique indexes.
Can I create a unique index if my collection already has duplicates?
Not successfully while conflicting index keys remain. Use an aggregation pipeline to group by the intended key and filter for counts greater than one, resolve the conflicts, and prevent new duplicates during the build. A failed build doesn't repair the data.
Does Mongoose unique: true validate duplicates?
No. It declares a MongoDB unique index, not a Mongoose validator. The index must be deployed, and your application must handle database duplicate-key errors, commonly identified by code 11000 and the E11000 message prefix.
Further Reading
- MongoDB unique indexes — constraint behavior, null keys, and restrictions.
- createIndex() reference — key patterns, uniqueness, and index options.
- Partial indexes — filter expressions and conditional uniqueness.
- Sparse indexes — missing-field inclusion and unique sparse indexes.
- Index builds on populated collections — concurrent writes, failures, and operational impact.
- getIndexes() reference — inspecting deployed keys and options.
- Shard key indexes — unique-index restrictions and the _id exception.
- Mongoose's unique option — why uniqueness isn't model validation.
