Unique, Partial, TTL and Wildcard Indexes

Intermediate
12 min

Unique, Partial, TTL and Wildcard Indexes

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.

Unique Indexes

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:

javascript
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_1

Points to remember:

  • A compound unique index constrains the combination of fields: { orgId: 1, slug: 1 } allows the same slug in different organisations.
  • A missing field counts as null, so only one document may lack the field. Combine with a partial filter ({ email: { $exists: true } }) when the field is optional.
  • Uniqueness is byte-exact unless you add a collation with strength: 2, which makes Ada@Example.com and ada@example.com collide.

Partial Indexes

partialFilterExpression indexes only the documents that satisfy a filter. The index is smaller, cheaper to maintain, and stays hot in memory:

javascript
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 it

The 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.

TTL Indexes: Automatic Expiry

A TTL (time-to-live) index deletes documents once a Date field is older than expireAfterSeconds:

javascript
// 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:

javascript
db.runCommand({ collMod: "sessions", index: { keyPattern: { lastSeenAt: 1 }, expireAfterSeconds: 7200 } })

Wildcard Indexes

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:

javascript
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.

Index Option Reference

| 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() |

Common Mistakes

  • Unique index on an optional field without a partial filter, so the second document without the field is rejected.
  • TTL on a string timestamp. Nothing expires and nothing warns you.
  • Dropping an index to see what breaks. Hide it first with db.orders.hideIndex("name"), watch for slow queries, then drop.
Quick Quiz
Question 1 of 3

Two documents have no `phone` field. What happens with `createIndex({ phone: 1 }, { unique: true })`?

Key Takeaways

  • 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.
  • TTL indexes delete documents after expireAfterSeconds on a Date field, checked roughly every minute; 0 enables per-document deadlines.
  • Wildcard indexes ("path.$**") handle unpredictable field names at the cost of some planner flexibility.
  • Hide an index before dropping it to confirm nothing depends on it.

Next lesson: Text Search: $regex, Text Indexes and Atlas Search — three ways to search strings, from simple patterns to relevance-ranked full-text search.

Unique, Partial, TTL and Wildcard Indexes - MongoDB | CodeYourCraft | CodeYourCraft