MongoDB Index Types: Which One Your Query Actually Needs
Single field, compound, multikey, text, wildcard, hashed, geospatial and TTL — what each is for, and the limits.
MongoDB has eight index types and most applications need three of them. The other five exist for specific shapes of data, and reaching for one when you did not need it costs write throughput for nothing.
Key takeaways#
- Single-field and compound indexes handle the overwhelming majority of queries.
- Multikey is not something you create — MongoDB makes an index multikey automatically when the field holds an array, and you cannot compound two array fields.
- A collection may hold only one text index, though it can span many fields.
- Wildcard indexes are for genuinely unpredictable key names, not as a substitute for knowing your access patterns.
- Every index is written on every insert, update and delete. The cost is real and continuous.
Single field#
The default. An ascending or descending index on one field:
db.orders.createIndex({ customerId: 1 });Direction is irrelevant for a single-field index — MongoDB traverses the structure in either direction, so { createdAt: 1 } serves sort({ createdAt: -1 }) perfectly well. Direction only starts to matter with multiple fields sorted in mixed directions.
Every collection has one of these on _id already, created automatically and impossible to drop.
Compound#
Up to 32 fields in a single index, and the order of those fields decides what the index can do:
db.orders.createIndex({ customerId: 1, createdAt: -1, total: 1 });A compound index serves any left-hand prefix of its keys. This one covers queries on customerId, and on customerId + createdAt, and on all three — but never on createdAt alone.
That prefix property is the main lever on index count in a collection. Three separate indexes on {customerId}, {customerId, createdAt} and all three fields are redundant; the widest one covers the other two.
Field order follows the ESR rule — equality fields, then sort fields, then range fields. Getting it backwards produces an index the planner uses for filtering but not for ordering, which means an in-memory sort and a hard 32MB ceiling.
Multikey#
You do not create a multikey index. MongoDB marks an index multikey the moment it encounters a document where the indexed field holds an array, and it then stores one index entry per array element.
// tags: ['mongodb', 'indexing']
db.articles.createIndex({ tags: 1 });Two entries are written for that document. A query for { tags: 'indexing' } seeks straight to it.
The constraint that catches people: a compound index may contain at most one array field. { tags: 1, categories: 1 } fails the moment a single document has arrays in both, because the number of entries would be the product of the two array lengths. The failure arrives on insert, not on index creation, so it can surface long after deployment.
Text#
Language-aware search across string fields — stemming, stop words, and relevance scoring:
db.articles.createIndex(
{ title: 'text', body: 'text' },
{ weights: { title: 10, body: 1 } },
);weights controls how much each field contributes to the relevance score, which is what makes a title match outrank a body match.
The limits are strict. One text index per collection, though it may cover any number of fields. Queries use $text rather than a field name, so you cannot restrict a search to one field at query time — the index decides that. And sorting by relevance requires projecting { score: { $meta: 'textScore' } } explicitly.
For anything resembling real search — typo tolerance, faceting, phrase proximity — a text index is not the tool. Atlas Search or a dedicated search engine is. A text index is for "find articles mentioning this word", and it does that well.
Wildcard#
Indexes every field in a document, or every field beneath a path:
db.events.createIndex({ 'metadata.$**': 1 });This is for genuinely unpredictable key names: user-defined custom fields, arbitrary event metadata, a settings blob whose shape varies per tenant. It is not a shortcut for skipping index design, and it should not be reached for because listing the fields felt tedious.
Wildcard indexes cannot be compound with regular fields in the general case, cannot enforce uniqueness, and index far more than a targeted index would — which means a bigger structure, slower writes, and more memory. When you know the fields, index the fields.
Hashed#
Stores a hash of the value rather than the value:
db.users.createIndex({ userId: 'hashed' });This exists almost entirely for sharding. A monotonically increasing shard key like an ObjectId or a timestamp sends every new document to the same chunk, creating a write hotspot on one shard; hashing distributes them evenly.
The trade is that hashing destroys ordering, so a hashed index cannot serve range queries or sorts. Outside of a shard key decision, it is rarely the right choice.
Geospatial#
Two variants. 2dsphere for real coordinates on a sphere, 2d for flat coordinates on a plane:
db.stores.createIndex({ location: '2dsphere' });2dsphere is what you want for anything on Earth — it understands GeoJSON, handles the curvature, and supports $near, $geoWithin and $geoIntersects. It expects GeoJSON objects, and a plain [lng, lat] array is interpreted as legacy coordinate pairs with subtly different semantics.
Note the order: GeoJSON is longitude first, which is the reverse of how coordinates are usually spoken. Reversed pairs produce queries that return nothing, or results in the wrong ocean.
2d is legacy, for flat non-Earth coordinate systems like a game map.
TTL#
A single-field index on a date that deletes documents once they age past a threshold:
db.sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });With expireAfterSeconds: 0, the field itself is the exact expiry time. The field must be a BSON date — a Unix timestamp stored as a number is ignored silently, with no error and no deletions. And the removal task runs roughly every 60 seconds, so documents can outlive their expiry briefly. Use it for cleanup, not for enforcing an access deadline.
Choosing#
| Situation | Index |
|---|---|
| Filter or sort on one field | Single field |
| Filter and sort together | Compound, ESR ordered |
| Field holds an array | Multikey — automatic |
| Word search in prose | Text — one per collection |
| Unpredictable key names | Wildcard |
| Even distribution for a shard key | Hashed |
| Coordinates on Earth | 2dsphere |
| Rows that should expire | TTL |
Start with compound indexes matching your actual query shapes. Add a specialised type only when a specific query cannot be served without it — and confirm with explain('executionStats') that the planner agrees with you.
Then check what nobody is using:
db.orders.aggregate([{ $indexStats: {} }]);An index with near-zero accesses.ops after real traffic is write cost with no read benefit. Drop it, unless it is enforcing uniqueness — a unique index is a constraint whether or not queries use it.
Both explain() and $indexStats return nested documents that are painful to read raw. The JSON Formatter is the quickest way to see what the planner chose.
Tags
- MongoDB
- Database
- Performance
- Indexing