MongoDB reads every document in a collection if there is no index — a collection scan. At 10 million documents, that is slow. Indexes fix this.

Single-Field Index

// Create an index on the email field
db.users.createIndex({ email: 1 })  // 1 = ascending, -1 = descending

// The query now uses the index
db.users.find({ email: "alex@example.com" })

// Unique index — enforces uniqueness
db.users.createIndex({ email: 1 }, { unique: true })

Compound Index

Index multiple fields together when you filter or sort on more than one:

// Index for filtering by user_id and sorting by createdAt
db.orders.createIndex({ user_id: 1, createdAt: -1 })

// Uses the index (matches leading fields)
db.orders.find({ user_id: "abc" }).sort({ createdAt: -1 })

// Also uses the index (partial match on leading field)
db.orders.find({ user_id: "abc" })

// Does NOT use the index (skips the leading field)
db.orders.find({ createdAt: { $gt: ISODate("2026-01-01") } })

The ESR rule for compound indexes: Equality fields first, then Sort fields, then Range fields.

// Query: status == "pending" AND createdAt in range, sorted by amount
// ESR order: equality (status) → sort (amount) → range (createdAt)
db.orders.createIndex({ status: 1, amount: 1, createdAt: 1 })

explain() — Analyze Query Plans

// See what MongoDB does for a query
db.orders.find({ user_id: "abc" }).explain("executionStats")

Key fields in the output:

{
  "queryPlanner": {
    "winningPlan": {
      "stage": "FETCH",          // or "COLLSCAN" = full scan (bad)
      "inputStage": {
        "stage": "IXSCAN",       // index scan (good)
        "indexName": "user_id_1"
      }
    }
  },
  "executionStats": {
    "totalDocsExamined": 5,      // low = good
    "totalDocsReturned": 5,
    "executionTimeMillis": 0,    // execution time
    "nReturned": 5
  }
}

If totalDocsExamined » nReturned, your index is not selective enough or is missing.

// Create a text index on one or more string fields
db.articles.createIndex({ title: "text", body: "text" })

// Or index all string fields with a wildcard
db.articles.createIndex({ "$**": "text" })

// Search
db.articles.find({ $text: { $search: "mongodb index performance" } })

// Rank by relevance score
db.articles.find(
  { $text: { $search: "mongodb index" } },
  { score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })

Only one text index per collection is allowed.

Geospatial Index

// Store coordinates as GeoJSON
db.places.insertOne({
  name: "Brandenburg Gate",
  location: {
    type: "Point",
    coordinates: [13.3777, 52.5163]  // [longitude, latitude]
  }
})

// Create 2dsphere index for GeoJSON
db.places.createIndex({ location: "2dsphere" })

// Find places within 1 km of a point
db.places.find({
  location: {
    $near: {
      $geometry: { type: "Point", coordinates: [13.3777, 52.5163] },
      $maxDistance: 1000  // meters
    }
  }
})

Partial Index

Index only documents that match a filter condition. Smaller, faster:

// Index only active users
db.users.createIndex(
  { email: 1 },
  { partialFilterExpression: { active: { $eq: true } } }
)

// Index only orders that aren't completed
db.orders.createIndex(
  { user_id: 1, createdAt: -1 },
  { partialFilterExpression: { status: { $ne: "completed" } } }
)

Sparse Index

A sparse index only includes documents that have the indexed field:

// Index only documents that have a phone field
db.users.createIndex({ phone: 1 }, { sparse: true })

Useful when many documents don’t have a field and you don’t want them indexed.

Check Index Usage

// List all indexes on a collection
db.orders.getIndexes()

// See index statistics (usage counts)
db.orders.aggregate([{ $indexStats: {} }])

Indexes with zero usage since the last mongod restart are candidates for removal. Unused indexes waste write performance and memory.

Node.js Example

import mongoose, { Schema } from "mongoose";

const orderSchema = new Schema({
  user_id:   { type: Schema.Types.ObjectId, ref: "User", required: true },
  status:    { type: String, required: true },
  amount:    { type: Number, required: true },
  createdAt: { type: Date, default: Date.now },
});

// Compound index in Mongoose schema
orderSchema.index({ user_id: 1, createdAt: -1 });
orderSchema.index({ status: 1 }, { partialFilterExpression: { status: { $ne: "completed" } } });

const Order = mongoose.model("Order", orderSchema);

// Check query performance (wrap in async function for compatibility)
async function checkPerformance() {
  const userId = new mongoose.Types.ObjectId("64b1234567890abcdef12345");
  const explain = await Order
    .find({ user_id: userId })
    .sort({ createdAt: -1 })
    .explain("executionStats");

  console.log(explain.executionStats.executionTimeMillis);
}

Python Example

from pymongo import MongoClient, ASCENDING, DESCENDING, TEXT

client = MongoClient("mongodb://admin:password@localhost:27017/")
db = client["myapp"]

# Create compound index
db.orders.create_index([("user_id", ASCENDING), ("created_at", DESCENDING)])

# Create text index
db.articles.create_index([("title", TEXT), ("body", TEXT)])

# Create geospatial index
db.places.create_index([("location", "2dsphere")])

# Analyze query
explain = db.orders.find({"user_id": "abc"}).explain()
print(explain["executionStats"]["executionTimeMillis"])

# List indexes
for index in db.orders.list_indexes():
    print(index["name"], index.get("key"))

What’s Next?

Your MongoDB queries are now fast. Let’s move to Redis — an in-memory database for caching, sessions, and real-time features.

Next: Database Tutorial #13: Redis Setup and Data Types