Indexes and explain
Introduction
Without indexes, MongoDB scans every document in a collection—a COLLSCAN. As data grows, unindexed queries become slow and expensive. This chapter creates single-field, compound, unique, and partial indexes, reads explain("executionStats"), and applies the ESR rule (Equality, Sort, Range) for compound index design.
Prerequisites
- Find Queries and Projection
- Query Operators
- Sample
hello_demo.postsdata — re-seed from chapter 5 if empty
Why Indexes Matter
Indexes are B-tree structures (by default) that map field values to document locations—similar in purpose to MySQL indexes, but support documents and multi-key (array) paths.
Trade-offs:
| Benefit | Cost |
|---|---|
| Faster reads | Slower writes (index maintenance) |
| Sorted scans from index | Disk and RAM for index data |
| Unique enforcement | Must choose keys carefully |
Create and Inspect Indexes
Index names default to field_direction concatenation.
Unique Index
Enforce one document per slug:
db.posts.createIndex({ slug: 1 }, { unique: true })Duplicate insert/update raises E11000 duplicate key error.
Partial unique (only published posts):
db.posts.createIndex(
{ slug: 1 },
{
unique: true,
partialFilterExpression: { published: true }
}
)Compound Indexes and ESR Rule
For query:
db.posts.find({ published: true, viewCount: { $gte: 50 } })
.sort({ createdAt: -1 })Prefer index field order:
- Equality —
published - Sort —
createdAt - Range —
viewCount
Example index:
db.posts.createIndex({ published: 1, createdAt: -1, viewCount: 1 })Not every query needs a perfect index—optimize the top slow queries from logs.
Multikey Indexes (Arrays)
Index on array field indexes each array element:
db.posts.createIndex({ tagIds: 1 })
db.posts.find({ tagIds: ObjectId("665f0123456789abcdef01234") })One compound index can include at most one array field (multikey limitation).
Covered Queries
Query satisfied entirely from index—no document fetch:
db.posts.createIndex({ slug: 1, title: 1 })
db.posts.find(
{ slug: "mongodb-basics" },
{ _id: 0, slug: 1, title: 1 }
).explain("executionStats")Look for totalDocsExamined: 0 in winning plan when fully covered.
explain("executionStats")
db.posts.find({ published: true }).sort({ createdAt: -1 }).explain("executionStats")Key fields in output:
| Field | Meaning |
|---|---|
winningPlan.stage | IXSCAN (index) vs COLLSCAN (full scan) |
totalDocsExamined | Documents scanned—lower is better |
totalKeysExamined | Index keys scanned |
executionTimeMillis | Server time (dev only indicator) |
Bad plan example:
stage: COLLSCAN
totalDocsExamined: 100000Add or fix index until IXSCAN with small totalDocsExamined.
Text Index (Simple Search)
db.posts.createIndex({ title: "text", body: "text" })
db.posts.find({ $text: { $search: "mongodb indexing" } })One text index per collection. For advanced search consider Atlas Search or Elasticsearch.
Partial Index
Index subset of documents—smaller, faster:
db.posts.createIndex(
{ createdAt: -1 },
{ partialFilterExpression: { published: true } }
)Only published posts appear in index.
TTL Index (Review)
From Delete and TTL—index on date + expireAfterSeconds. Do not duplicate TTL and query index without planning—often combine one TTL field index.
Index Anti-Patterns
| Problem | Symptom | Fix |
|---|---|---|
| No index on filter field | COLLSCAN | createIndex on filter/sort |
| Too many indexes | Slow writes | Drop unused (Atlas index suggestions) |
| Wrong compound order | Index not used | Apply ESR; check explain |
| Unanchored regex | COLLSCAN | Anchor prefix or text index |
$ne, $nin | Poor index use | Redesign query or partial index |
Workflow for Slow Query
- Reproduce with
explain("executionStats") - Identify
COLLSCANor hightotalDocsExamined - Add compound index matching filter + sort
- Re-run explain; verify
IXSCAN - Monitor in production — Performance
Post-Chapter Checklist
-
createIndexonslug(unique) and query patterns you use - Ran
explainbefore and after index - You can read
IXSCANvsCOLLSCAN - You understand index write overhead
FAQ
_id index enough?
_id helps find({ _id }) only. Add indexes for slug, userId, etc.
Index every field?
No—index fields that appear in selective filters and sorts.
hideIndex vs dropIndex?
hideIndex (4.4+) test drop impact before removing.
Sparse index?
Indexes only docs with field present—largely replaced by partial indexes.
Same as Redis?
Redis has no user-defined B-tree indexes on documents—different product — Redis.