Why indexes matter
A query that returns the right answer can still be a slow query. When you ask MongoDB to find every member who joined in 2023, and there is nothing helping it, the server has only one option: open the collection at the first document and read forward, checking each one, until it has looked at every single document. That brute-force read is called a collection scan, and MongoDB labels it COLLSCAN in its diagnostics. On a handful of library members it is invisible. On a collection of ten million documents it is the difference between a reply in a millisecond and a reply that crawls.
An index is the fix. It is a separate, ordered data structure that the server maintains alongside your collection, holding the values of one or more fields in sorted order, each paired with a pointer back to the full document. Because the values are sorted, the server can jump straight to the ones it wants instead of reading everything — the same reason you flip to the back of a textbook rather than reading every page to find where a term is defined. This whole module is about building the right index for the right query, and proving the win with explain.
A scan versus a seek
Section titled “A scan versus a seek”Picture the two strategies side by side. Without an index, the query walks the entire collection. With an index, it descends a sorted tree and lands directly on the matching range:
flowchart TB
subgraph NoIndex["Without an index — COLLSCAN"]
Q1["Query: joined equals 2023"] --> S0["Read doc 1"]
S0 --> S1["Read doc 2"]
S1 --> S2["Read doc 3"]
S2 --> S3["...read every document"]
S3 --> R1["Return the matches"]
end
subgraph WithIndex["With an index — IXSCAN"]
Q2["Query: joined equals 2023"] --> B0["Index root"]
B0 --> B1["Sorted branch"]
B1 --> B2["Leaf: joined equals 2023"]
B2 --> R2["Follow pointers to matching docs"]
end The right-hand path touches only the documents it needs. The left-hand path touches all of them and then throws most away. That wasted reading is exactly what an index removes.
A first index
Section titled “A first index”Creating an index is a single call. You name the field and a direction — 1 for ascending — and the server builds the structure for you. Here we index the joined field on our library members so that filtering or sorting by join year becomes a seek:
db.members.createIndex({ joined: 1 })await db.collection("members").createIndex({ joined: 1 });db.members.create_index([("joined", 1)])_, err := coll.Indexes().CreateOne( ctx, mongo.IndexModel{Keys: bson.D{{Key: "joined", Value: 1}}},)if err != nil { return err}coll.create_index( IndexModel::builder().keys(doc! { "joined": 1 }).build(),).await?;The call returns the name MongoDB gave the new index — it derives the name from the field and direction unless you supply your own:
{ "createdIndexes": ["joined_1"], "ok": 1 }In Compass: open a collection and choose the Indexes tab. You will always see the built-in _id_ index there, and you can add a new one with the Create Index button by typing the field name and picking a direction.
What this module covers
Section titled “What this module covers”The lessons build from one field to the full toolbox:
- Why indexes matter — scans versus seeks, and the cost of having none.
- Single and compound indexes — indexing one field, then several, and the prefix rule that decides which queries a compound index can serve.
- The ESR rule — the field ordering (Equality, Sort, Range) that makes a compound index pull its weight.
- Explain and query plans — reading
explainoutput to confirm an index was used and to spot a covered query. - Index types — multikey, text, TTL, partial, unique, and sparse indexes for the cases a plain index cannot cover.
Tips and gotchas
Section titled “Tips and gotchas”- Every collection starts with one index already: a unique index on
_id. You never have to create it, and you cannot drop it. - Indexes are not free. They speed up reads but add work to every insert, update, and delete, because the index must be kept in sync. Index the fields your queries actually use, not every field.
- An index lives on disk and in memory like the data it describes. A pile of unused indexes wastes both, so it is worth removing ones no query relies on.