Index Types
MongoDB supports many specialized index types beyond the basic single-field index. Each type is optimized for specific data patterns or query types — choosing the right one has a direct impact on both query performance and storage efficiency.
Single Field Index
The most common index type. Indexes a single field in ascending (1) or descending (-1) order. For single-field indexes, the direction matters only when sorting — for equality and range queries, either direction works equally well.
// Ascending — good for range queries, low-to-high sorts
db.users.createIndex({ email: 1 })
// Descending — efficient for newest-first queries
db.posts.createIndex({ publishedAt: -1 })
// Unique single-field index
db.accounts.createIndex({ apiKey: 1 }, { unique: true })Compound Index
A compound index spans multiple fields in a single index structure. It can serve queries on any prefix of the indexed fields — making one compound index far more versatile than several single-field indexes.
Compound index
// Compound index on lastName, then firstName
db.users.createIndex({ lastName: 1, firstName: 1 })
// This ONE index can efficiently serve ALL of these queries:
db.users.find({ lastName: "Smith" })
db.users.find({ lastName: "Smith", firstName: "Alice" })
db.users.find({ lastName: "Smith" }).sort({ firstName: 1 })
// It CANNOT efficiently serve:
db.users.find({ firstName: "Alice" }) // firstName is not a prefix
// Another example: userId + status + createdAt
db.orders.createIndex({ userId: 1, status: 1, createdAt: -1 })
// Serves: { userId } queries, { userId, status } queries,
// { userId, status, createdAt } queries, and sort by createdAt
// when filtered by userId (and optionally status){ a, b, c } can serve queries on { a }, { a, b }, and { a, b, c }. It CANNOT efficiently serve queries on just { b } or { b, c } — those fields are not index prefixes.Multikey Index — Arrays
When you create an index on a field that holds an array value, MongoDB automatically creates a multikey index — it stores one index entry per element in the array. This allows efficient queries that match any array element.
Multikey index
// Documents in the articles collection look like:
// { _id: ..., title: "...", tags: ["mongodb", "database", "nosql"] }
// Create an index on the tags array field
db.articles.createIndex({ tags: 1 })
// MongoDB automatically creates a MULTIKEY index — one entry per tag
// Query: find all articles tagged "mongodb"
db.articles.find({ tags: "mongodb" })
// Query: find articles tagged with all of these
db.articles.find({ tags: { $all: ["mongodb", "database"] } })
// Query: find articles tagged with any of these
db.articles.find({ tags: { $in: ["mongodb", "nosql"] } })
// Compound multikey index (one array field allowed in compound)
db.articles.createIndex({ authorId: 1, tags: 1 })createIndex({ tags: 1, categories: 1 }) will fail if both tags and categories contain arrays in the same document.Text Index
Text indexes support full-text search queries using the $text operator. They tokenize string content, remove stop words, and apply stemming. A collection may have at most one text index, but it can span multiple fields. See the Text Search page for full coverage.
// Multi-field text index with field weights
db.articles.createIndex(
{ title: "text", body: "text" },
{ weights: { title: 10, body: 1 } }
)
// Search
db.articles.find({ $text: { $search: "mongodb performance" } })Geospatial Indexes — 2dsphere
2dsphere indexes support queries on GeoJSON data — points, lines, and polygons stored on a spherical model of the Earth. Use this for location-aware queries like "find all restaurants within 1 km of this point."
2dsphere geospatial index
// Documents store GeoJSON location:
// { name: "Cafe Roma", location: { type: "Point", coordinates: [13.405, 52.52] } }
// coordinates are [longitude, latitude]
// Create a 2dsphere index
db.restaurants.createIndex({ location: "2dsphere" })
// $near — find restaurants within 1 km, sorted by distance
db.restaurants.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [13.405, 52.52] // [lng, lat]
},
$maxDistance: 1000, // 1000 metres = 1 km
$minDistance: 0
}
}
})
// $geoWithin — all locations inside a polygon
db.restaurants.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [[
[13.38, 52.51],
[13.45, 52.51],
[13.45, 52.54],
[13.38, 52.54],
[13.38, 52.51] // close the ring
]]
}
}
}
})2d Index (Legacy Flat Coordinates)
The legacy 2d index works with simple [x, y] flat coordinate pairs — not GeoJSON. Use 2dsphere for new applications. The 2d index only makes sense for data on a flat plane (e.g., a game map with pixel coordinates).
2d index for flat coordinates
// Documents: { name: "spawn_point", coords: [120, 450] }
// Create 2d index
db.gameMap.createIndex({ coords: "2d" })
// $geoWithin $box — find all points within a rectangle
db.gameMap.find({
coords: {
$geoWithin: {
$box: [
[100, 400], // bottom-left corner
[200, 500] // top-right corner
]
}
}
})
// $near on a flat grid
db.gameMap.find({
coords: { $near: [120, 450], $maxDistance: 50 }
})Hashed Index — Sharding
A hashed index computes a hash of the field value and stores the hash. They are used primarily as shard keys in sharded clusters to achieve an even distribution of data across shards — avoiding hotspots that occur with monotonically increasing values like timestamps or auto-incremented IDs.
Hashed index
// Create a hashed index — typically used as a shard key
db.events.createIndex({ userId: "hashed" })
// Shard a collection using the hashed index as shard key
sh.shardCollection("mydb.events", { userId: "hashed" })
// Hashed indexes support only equality queries:
db.events.find({ userId: "user_abc123" }) // fast
// Range queries do NOT benefit from hashed indexes:
db.events.find({ userId: { $gt: "user_a" } }) // COLLSCAN$gt, $lt, $in with ranges). They are for equality lookups and even data distribution in sharded clusters only.Sparse Index
A sparse index only includes documents that have the indexed field — it skips documents where the field is absent or null. This is useful for optional fields where you don't want null/missing values filling up the index, and it enables unique constraints on optional fields.
Sparse index
// Unique sparse index on an optional email field
// Documents without an email field are NOT indexed (and not checked for uniqueness)
db.users.createIndex(
{ email: 1 },
{ unique: true, sparse: true }
)
// Without sparse: a unique index on an optional field would
// fail when a second document has no email (both have null)
// A sparse index may NOT be used if the query must return docs
// where the field is missing — MongoDB will fall back to COLLSCAN
db.users.find({ email: { $exists: false } })
// The sparse index on email won't help here; MongoDB scans the collectionPartial Index
A partial index indexes only the subset of documents matching a filter expression. This produces a smaller, more efficient index compared to indexing the full collection — and reduces write overhead since many documents won't need index updates.
Partial index
// Only index active users — inactive users are excluded
db.users.createIndex(
{ email: 1 },
{
partialFilterExpression: { status: "active" },
unique: true
}
)
// Only index products that are in stock
db.products.createIndex(
{ price: 1, category: 1 },
{
partialFilterExpression: { inStock: true }
}
)
// Only index high-value orders
db.orders.createIndex(
{ customerId: 1, createdAt: -1 },
{
partialFilterExpression: { total: { $gt: 1000 } }
}
)
// IMPORTANT: the query must include the partial filter expression
// for MongoDB to use this index
db.users.find({ email: "alice@example.com", status: "active" }) // uses index
db.users.find({ email: "alice@example.com" }) // COLLSCANTTL Index — Auto-Expiry
A TTL (Time To Live) index automatically deletes documents after a specified number of seconds have elapsed since the value of the indexed date field. MongoDB's background TTL monitor runs every 60 seconds, so actual deletion may lag by up to a minute.
TTL index
// Auto-delete session documents 2 hours after createdAt
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 7200 } // 7200 seconds = 2 hours
)
// Auto-delete log entries 30 days after timestamp
db.logs.createIndex(
{ timestamp: 1 },
{ expireAfterSeconds: 2592000 } // 30 days
)
// Expire at a specific time using an absolute expiry field
// Set expiresAt to a specific Date in each document
db.jobs.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 } // expire AT the field value, not after it
)
// Document: { task: "cleanup", expiresAt: new Date("2025-12-31T23:59:59Z") }
// Modify the TTL duration after creation
db.runCommand({
collMod: "sessions",
index: { keyPattern: { createdAt: 1 }, expireAfterSeconds: 3600 }
})Wildcard Index
Wildcard indexes index all fields in a document — or all fields under a subdocument path. They are designed for workloads with unpredictable or user-defined field names, such as storing arbitrary metadata, form responses, or IoT sensor readings where field names vary by device type.
Wildcard index
// Index ALL fields in every document
db.products.createIndex({ "$**": 1 })
// Index only fields under the "metadata" subdocument
db.products.createIndex({ "metadata.$**": 1 })
// Documents can now have arbitrary metadata:
// { name: "Widget A", metadata: { color: "red", weight: 1.2 } }
// { name: "Widget B", metadata: { voltage: 5, frequency: 60 } }
// Both queries can use the wildcard index:
db.products.find({ "metadata.color": "red" })
db.products.find({ "metadata.voltage": { $gt: 3 } })
// Exclude specific fields from a wildcard index
db.products.createIndex(
{ "$**": 1 },
{
wildcardProjection: {
largeTextField: 0, // exclude this field
binaryData: 0
}
}
)Index Type Summary
Index Type | Best For | Limitation |
|---|---|---|
Single Field | Simple equality/range queries on one field | Cannot cover multi-field query patterns efficiently |
Compound | Multi-field queries, covering queries, ESR-ordered access patterns | Only prefix queries can use the index; field order matters |
Multikey | Querying documents by array element values | Compound index cannot have two multikey fields |
Text | Full-text keyword search with stop word removal | One per collection; no fuzzy match; no prefix match |
2dsphere | GeoJSON location queries on a sphere (real-world coordinates) | GeoJSON only; no flat coordinate support |
2d | Legacy flat [x,y] coordinate queries | Deprecated for new apps; no GeoJSON support |
Hashed | Even data distribution for sharding shard keys | No range queries; equality only |
Sparse | Optional fields where missing should not be indexed | Cannot be used for queries returning docs missing the field |
Partial | Indexing a filtered subset of documents | Query must include the partial filter expression to use it |
TTL | Automatic document expiry (sessions, logs, cache) | Only works on Date fields; ~60s deletion lag |
Wildcard | Unpredictable / user-defined field names | Less efficient than targeted compound indexes for known fields |
{ userId: 1, createdAt: -1 } is more efficient than two separate indexes on userId and createdAt — it avoids index intersection, uses less memory, and can serve covered queries.