MongoDB Indexes: Types, Design, and Performance Tuning
Learn MongoDB index types: single-field, compound, multikey, text, and geospatial. Use explain() to verify index usage and design indexes for your access patterns. Lesson 7 of MongoDB & NoSQL Mastery.

A collection scan (COLLSCAN) reads every document to find matches — fine for a hundred documents, catastrophic for a million. Indexes let MongoDB jump directly to matching documents. Designing the right indexes for your access patterns is the single most impactful performance intervention available in MongoDB.
Previous: Lesson 6 — The Aggregation Pipeline
How MongoDB Indexes Work
MongoDB indexes are B-tree data structures stored separately from the collection data. When a query includes a filter or sort that matches an index, MongoDB uses an IXSCAN (index scan) instead of a COLLSCAN (collection scan).
Every collection has a default index on _id. Additional indexes must be created explicitly.
Index Types
Single-Field Index
// Ascending index on email
db.users.createIndex({ email: 1 })
// Descending index (identical performance for single-field indexes)
db.users.createIndex({ createdAt: -1 })
// Index with options
db.users.createIndex(
{ email: 1 },
{ unique: true, name: "unique_email" }
)Compound Index
A compound index covers multiple fields. Field order matters: MongoDB can use the index for queries on the first field, the first two fields, the first three, and so on — but not for queries that skip the first field (left-prefix rule).
// Index on status, then createdAt
db.orders.createIndex({ status: 1, createdAt: -1 })
// This query uses the index ✓
db.orders.find({ status: "shipped" }).sort({ createdAt: -1 })
// This also uses the index (leading field matches) ✓
db.orders.find({ status: "shipped" })
// This does NOT use the index (skips status) ✗
db.orders.find({}).sort({ createdAt: -1 })ESR Rule for compound indexes: Equality fields first, Sort fields second, Range fields last.
// Query: { status: "shipped", amount: { $gte: 100 } } sorted by createdAt
// Correct index order: equality → sort → range
db.orders.createIndex({ status: 1, createdAt: -1, amount: 1 })Multikey Index
When you index an array field, MongoDB creates a multikey index — one index entry per array element:
db.products.createIndex({ tags: 1 })
// Now this query uses the index
db.products.find({ tags: "wireless" })Note: A compound index cannot have more than one multikey field.
Text Index
Supports full-text search on string fields:
// Create a text index on one or more fields
db.articles.createIndex({ title: "text", body: "text" })
// Weighted text index (title matches count more)
db.articles.createIndex(
{ title: "text", body: "text" },
{ weights: { title: 10, body: 1 }, name: "article_text" }
)
// Query using $text
db.articles.find({ $text: { $search: "mongodb indexing performance" } })
// Sort by relevance score
db.articles.find(
{ $text: { $search: "mongodb" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })Only one text index per collection is allowed. For production full-text search, consider MongoDB Atlas Search (built on Lucene).
Geospatial Index
// 2dsphere index for GeoJSON data
db.locations.createIndex({ coordinates: "2dsphere" })
// Find locations within 5km of a point
db.locations.find({
coordinates: {
$near: {
$geometry: { type: "Point", coordinates: [-73.935242, 40.730610] },
$maxDistance: 5000
}
}
})Sparse and Partial Indexes
// Sparse: only index documents where the field exists
db.users.createIndex({ phone: 1 }, { sparse: true })
// Partial: index only documents matching a filter expression
db.orders.createIndex(
{ customerId: 1, createdAt: -1 },
{ partialFilterExpression: { status: { $in: ["shipped", "processing"] } } }
)
// Smaller, faster index — only active orders are indexedTTL Index — Automatic Document Expiry
// Delete session documents 1 hour after createdAt
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 }
)Analysing Queries with explain()
explain() reveals how MongoDB executes a query. Always use it when investigating slow queries.
// Verbosity levels: "queryPlanner", "executionStats", "allPlansExecution"
db.orders.find({ status: "shipped" }).explain("executionStats")Key Fields to Check
executionStats.executionStages.stage
COLLSCAN → full collection scan — needs an index
IXSCAN → index scan — good
FETCH → retrieve full documents after index scan — expected
SORT → in-memory sort — add the sort field to your index
executionStats.totalDocsExamined → documents read
executionStats.totalKeysExamined → index entries scanned
executionStats.nReturned → documents returned
Healthy ratio: nReturned ≈ totalDocsExamined
Unhealthy: nReturned << totalDocsExamined (scanning too many docs)Before and After Example
// Without index: 50,000 docs examined for 12 results
db.orders.find({ status: "shipped", customerId: ObjectId("user_123") })
.explain("executionStats")
// totalDocsExamined: 50000, nReturned: 12 → COLLSCAN
// Create index
db.orders.createIndex({ customerId: 1, status: 1 })
// With index: 12 docs examined for 12 results
db.orders.find({ status: "shipped", customerId: ObjectId("user_123") })
.explain("executionStats")
// totalDocsExamined: 12, nReturned: 12 → IXSCANCovered Queries
A covered query is satisfied entirely from the index — MongoDB never reads the collection data:
// Index covers name and price
db.products.createIndex({ brand: 1, name: 1, price: 1 })
// Covered query — only indexed fields in filter and projection, _id excluded
db.products.find(
{ brand: "Logitech" },
{ name: 1, price: 1, _id: 0 }
).explain("executionStats")
// totalDocsExamined: 0 (reads from index only)Managing Indexes
// List all indexes on a collection
db.orders.getIndexes()
// Index sizes
db.orders.stats().indexSizes
// Drop a specific index by name
db.orders.dropIndex("unique_email")
// Drop all non-_id indexes
db.orders.dropIndexes()
// Hide an index (test removing it without actually dropping it — MongoDB 4.4+)
db.orders.hideIndex("status_1_createdAt_-1")
// Run your workload, check explain() output, then drop or unhide
db.orders.unhideIndex("status_1_createdAt_-1")Index Design Guidelines
| Guideline | Rationale |
|---|---|
| Build indexes for your actual queries, not hypothetically | Every index costs write performance and memory |
Use explain("executionStats") to verify index usage | A query plan that looks correct may still COLLSCAN |
| Follow the ESR rule for compound indexes | Equality → Sort → Range field ordering |
| Avoid indexing high-cardinality fields with low selectivity | An index on a boolean field with 50/50 distribution is often useless |
| Use partial indexes for filtered workloads | Smaller index, faster writes for unindexed documents |
Monitor with MongoDB Atlas Performance Advisor or currentOp() | Catches slow queries automatically in production |
