Indexing & Performance
Checking access...
Indexes are the #1 performance optimization in MongoDB. Without an index, MongoDB must scan every document in a collection (collection scan). With the right index, queries can find data in microseconds.
Why Indexes Matter
Without index:
COLLSCAN — scans 1,000,000 documents to find 10 matchingTime: 2.3 secondsWith index:
IXSCAN — reads 10 index entries, fetches 10 documentsTime: 3 millisecondsWhen Indexes Help
- Queries you run frequently
- Sort operations on large datasets
- Fields used in
$lookup(JOIN) conditions - Fields with high cardinality (many unique values)
When Indexes Hurt
- Write-heavy collections (each write updates indexes too)
- Low-cardinality fields (e.g., boolean
isActive— 50% of documents) - Unused indexes (wasted memory and write overhead)
Single Field Index
// Create an ascending index on emaildb.users.createIndex({ email: 1 });
// Create a descending index on createdAtdb.users.createIndex({ createdAt: -1 });For a single-field index, sort direction rarely matters (MongoDB can traverse in either direction):
db.users.createIndex({ age: 1 });
// Both queries use the indexdb.users.find({ age: 30 }); // Exact matchdb.users.find({ age: { $gt: 25 } }).sort({ age: 1 }); // Range + sortCompound Index
A compound index on multiple fields. Field order matters significantly:
// Compound index: first by status, then by createdAtdb.orders.createIndex({ status: 1, createdAt: -1 });ESR Rule (Equality, Sort, Range)
For compound indexes, order fields by:
- Equality — fields checked with
$eq(exact matches) - Sort — fields used in
.sort() - Range — fields with range operators (
$gt,$lt,$in)
// Query: find active orders created in the last 7 days, sorted by totaldb.orders.find({ status: "active", // Equality createdAt: { $gte: sevenDaysAgo }, // Range}).sort({ total: -1 }); // Sort
// Optimal index (ESR order):db.orders.createIndex({ status: 1, total: -1, createdAt: 1 });// ^sort ^rangeIndex Prefixes
A compound index can serve queries that use a prefix of its fields:
db.orders.createIndex({ a: 1, b: 1, c: 1 });
// These queries use the index:db.orders.find({ a: 1 }); // Uses prefix [a]db.orders.find({ a: 1, b: 2 }); // Uses prefix [a, b]db.orders.find({ a: 1, b: 2, c: 3 }); // Uses full index
// These do NOT use the index efficiently:db.orders.find({ b: 2 }); // Missing prefix 'a'db.orders.find({ c: 3 }); // Missing prefixes 'a' and 'b'Multikey Index
MongoDB automatically creates a multikey index when you index an array field:
// Array field{ name: "Laptop", tags: ["electronics", "computers"] }
// Create index on array fielddb.products.createIndex({ tags: 1 });
// Queries using multikey indexdb.products.find({ tags: "electronics" });db.products.find({ tags: { $all: ["electronics", "computers"] } });Key constraints:
- Only one array field per index
- Multikey indexes cannot be used for covered queries
Text Index
For full-text search on string content:
// Create text index on one fielddb.articles.createIndex({ content: "text" });
// Create compound text index (multiple fields)db.articles.createIndex({ title: "text", content: "text", tags: "text",}, { weights: { title: 10, // Title matches are 10x more important content: 5, // Content matches are 5x more important tags: 1, }, name: "articles_text_index",});Text Search Queries
// Basic text searchdb.articles.find({ $text: { $search: "mongodb indexing" }});
// Text search with relevance scoredb.articles.find( { $text: { $search: "mongodb performance" } }, { score: { $meta: "textScore" } }).sort({ score: { $meta: "textScore" } });
// Exclude term (prepend with -)db.articles.find({ $text: { $search: "mongodb -sql" }});
// Exact phrase (wrap in quotes)db.articles.find({ $text: { $search: '"acid compliance" mongodb' }});Unique Index
Enforces unique values (like a primary key):
// Unique index on emaildb.users.createIndex({ email: 1 }, { unique: true });
// Unique compound indexdb.users.createIndex({ firstName: 1, lastName: 1 }, { unique: true });Sparse Index
Only indexes documents that have the indexed field:
// Only index documents with a "phone" fielddb.users.createIndex({ phone: 1 }, { sparse: true });
// Useful for optional fields — keeps index smallTTL Index
Documents automatically expire after a specified time:
// Delete documents 7 days after their createdAt fielddb.sessions.createIndex( { createdAt: 1 }, { expireAfterSeconds: 7 * 24 * 60 * 60 });
// Useful for: session stores, temporary data, event logsGeospatial Index
For location-based queries:
// 2dsphere index for GeoJSON datadb.places.createIndex({ location: "2dsphere" });
// Sample document{ name: "Central Park", location: { type: "Point", coordinates: [-73.9654, 40.7829] // [longitude, latitude] }}
// Find nearby placesdb.places.find({ location: { $near: { $geometry: { type: "Point", coordinates: [-73.97, 40.78] }, $maxDistance: 1000, // meters $minDistance: 0, } }});Viewing & Analyzing Indexes
List Indexes
// All indexes on a collectiondb.users.getIndexes();
// Output[ { v: 2, key: { _id: 1 }, name: "_id_" }, { v: 2, key: { email: 1 }, name: "email_1", unique: true }, { v: 2, key: { status: 1, createdAt: -1 }, name: "status_1_createdAt_-1" },]Index Size
// Index statisticsdb.users.stats().indexSizes;
// Output{ _id_: 81920, email_1: 40960, status_1_createdAt_-1: 122880,}Drop Indexes
// Drop by namedb.users.dropIndex("email_1");
// Drop all non-_id indexesdb.users.dropIndexes();Explain Plans
The most important tool for understanding query performance:
db.users.find({ email: "alice@example.com" }).explain("executionStats");Explain Output
{ "queryPlanner": { "winningPlan": { "stage": "FETCH", "inputStage": { "stage": "IXSCAN", // Index scan (good!) "indexName": "email_1", "indexBounds": { "email": ["[\"alice@example.com\", \"alice@example.com\"]"] } } } }, "executionStats": { "executionSuccess": true, "nReturned": 1, "totalDocsExamined": 1, // Only 1 document (excellent) "totalKeysExamined": 1, // Only 1 index entry "executionTimeMillis": 2, "stage" : "COLLSCAN", // Collection scan (bad!) "totalDocsExamined": 1000000, // Scanned ALL documents }}What to Look For
| Signal | Meaning | Action |
|---|---|---|
IXSCAN | Using index | ✅ Good |
COLLSCAN | Full collection scan | ❌ Add index |
totalDocsExamined ≈ nReturned | Efficient query | ✅ |
totalDocsExamined ≫ nReturned | Scanning many docs | ❌ Needs index |
SORT stage | In-memory sort | ❌ Add index with sort field |
FETCH after IXSCAN | Normal index usage | ✅ |
hint()
Force a specific index:
db.users.find({ email: "alice@example.com" }).hint({ email: 1 });Identifying Slow Queries
Profiling Levels
// Enable profiling (0=off, 1=slow ops, 2=all ops)db.setProfilingLevel(1, { slowms: 100 }); // Log ops > 100ms
// Check profiling leveldb.getProfilingStatus();
// View slow queriesdb.system.profile.find().sort({ ts: -1 }).limit(5).pretty();Profiling Output
{ "op": "query", "ns": "mydb.orders", "query": { "status": "pending" }, "nreturned": 50, "nscanned": 50000, "millis": 850, "ts": ISODate("2024-01-15T10:30:00Z"), "responseLength": 1234}Index Best Practices
1. Index for Your Query Patterns
// If you query by userId + sort by dateconst queryPattern = { userId: "123" };const sortPattern = { createdAt: -1 };
// Best indexdb.posts.createIndex({ userId: 1, createdAt: -1 });2. Covered Queries
When all needed fields are in the index (no FETCH step):
// Indexdb.users.createIndex({ email: 1, name: 1, age: 1 });
// Covered query — only reads from index, no document fetchdb.users.find( { email: "alice@example.com" }, { _id: 0, name: 1, age: 1 } // All in index!);
// Explain will show: stage: "IXSCAN" (no FETCH)3. Monitor Index Usage
// If using MongoDB Atlas: Performance Advisor// If self-hosted: $indexStats aggregationdb.users.aggregate([ { $indexStats: {} }]);
// Output shows usage counts per index4. Remove Unused Indexes
// Check which indexes are useddb.users.aggregate([ { $indexStats: {} }, { $match: { accesses: { ops: { $lt: 10 } } } }]);
// Drop unused indexes (0 ops = never used)db.users.dropIndex("unused_index");Quick Reference
// Create indexesdb.collection.createIndex({ field: 1 }) // Ascendingdb.collection.createIndex({ field: -1 }) // Descendingdb.collection.createIndex({ a: 1, b: 1 }) // Compounddb.collection.createIndex({ field: 1 }, { unique: true }) // Uniquedb.collection.createIndex({ field: 1 }, { sparse: true }) // Sparsedb.collection.createIndex({ field: "text" }) // Textdb.collection.createIndex({ loc: "2dsphere" }) // Geospatial
// Analyzedb.collection.find({}).explain("executionStats")db.collection.getIndexes()db.collection.dropIndex("index_name")
// Profiledb.setProfilingLevel(1, { slowms: 100 })db.system.profile.find().sort({ ts: -1 }).limit(5)Practice Exercises
Index creation: Create a
orderscollection with 100,000 documents (use a loop in mongosh). Query bystatus— note the time. Create an index onstatusand query again. Compare execution times usingexplain("executionStats").ESR index design: For the query
db.orders.find({ status: "shipped", total: { $gt: 100 } }).sort({ createdAt: -1 }), design and create the optimal compound index following the ESR rule. Verify withexplain.Text search: Create a text index on an
articlescollection with fieldstitleandbody, giving title double weight. Run text searches and observe how title matches rank higher.Profiling setup: Enable profiling for operations slower than 50ms. Run a slow query, then read from
system.profileto see the captured entry. Use this information to add the right index.