Beyond speeding up reads, indexes in MongoDB carry options that enforce constraints, shrink the index to the documents that matter, delete data on a schedule, and cover fields whose names you cannot predict. After this lesson you will be able to choose and configure unique, partial, TTL and wildcard indexes, and combine their options correctly.
A unique index rejects any insert or update that would create a second document with the same key. The _id index is unique by default; add your own for natural keys:
db.users.createIndex({ email: 1 }, { unique: true })
db.users.insertOne({ email: "ada@example.com" })
db.users.insertOne({ email: "ada@example.com" })
// MongoServerError: E11000 duplicate key error collection: shop.users index: email_1Points to remember:
{ orgId: 1, slug: 1 } allows the same slug in different organisations.null, so only one document may lack the field. Combine with a partial filter ({ email: { $exists: true } }) when the field is optional.strength: 2, which makes Ada@Example.com and ada@example.com collide.partialFilterExpression indexes only the documents that satisfy a filter. The index is smaller, cheaper to maintain, and stays hot in memory:
db.orders.createIndex(
{ customerId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "open" } }
)
db.orders.find({ customerId: 42, status: "open" }).sort({ createdAt: -1 }) // uses it
db.orders.find({ customerId: 42 }) // cannot use itThe planner uses a partial index only when the query's own conditions guarantee the filter, so include status: "open" in every query that should hit it. Filters may use equality, $exists, $gt/$gte/$lt/$lte, $type, $and and (on recent versions) $in and $or.
A TTL (time-to-live) index deletes documents once a Date field is older than expireAfterSeconds:
// Delete when lastSeenAt + 1 hour has passed
db.sessions.createIndex({ lastSeenAt: 1 }, { expireAfterSeconds: 3600 })
// Per-document expiry: store the exact moment and set the delay to 0
db.tokens.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
db.tokens.insertOne({ token: "abc", expireAt: new Date(Date.now() + 15 * 60 * 1000) })A background thread runs every 60 seconds, so removal happens shortly after the deadline rather than at the exact second. Documents whose field is missing or is not a Date never expire; a string like "2026-03-01" will silently keep the document forever. A TTL index must be a single-field index on a non-_id field, and the delay can be changed later with collMod:
db.runCommand({
collMod: "sessions",
index: { keyPattern: { lastSeenAt: 1 }, expireAfterSeconds: 7200 }
})When documents carry arbitrary attribute names — product specifications, user-defined metadata — you cannot create an index per field. A wildcard index covers every field under a path:
db.products.createIndex({ "attributes.$**": 1 })
db.products.find({ "attributes.color": "red" }) // uses the wildcard index
db.products.find({ "attributes.weightKg": { $lt: 5 } }){ "$**": 1 } indexes the whole document; wildcardProjection includes or excludes specific paths. Wildcard indexes serve single-field predicates well but cannot enforce uniqueness, and they support sorting only when the query also filters on the same field. Since MongoDB 7.0 a wildcard term can be combined with regular fields in a compound index.
| Option | Purpose | Notes |
|---|---|---|
| unique: true | reject duplicate keys | null counts as a value |
| partialFilterExpression | index a subset of documents | query must imply the filter |
| expireAfterSeconds | delete expired documents | field must hold a Date |
| "path.$**" key | index unknown field names | single-field predicates |
| hidden: true | keep index, hide it from the planner | test removal safely with hideIndex() |
db.orders.hideIndex("name"), watch for slow queries, then drop.Two documents have no `phone` field. What happens with `createIndex({ phone: 1 }, { unique: true })`?
unique: true enforces one document per key; combine with a collation for case-insensitive keys and with a partial filter for optional fields.partialFilterExpression indexes a subset of documents; queries must include the filter condition to use it.expireAfterSeconds on a Date field, checked roughly every minute; 0 enables per-document deadlines."path.$**") handle unpredictable field names at the cost of some planner flexibility.Next lesson: Text Search: $regex, Text Indexes and Atlas Search — three ways to search strings, from simple patterns to relevance-ranked full-text search.