Manage the complete index lifecycle with definitions, usage statistics, hidden-index experiments, correct drop/recreate renaming, and measured retirement decisions.
Create, Hide, Inspect, Rename/Drop, and Measure Indexes Without Guessing
Batch heterogeneous writes safely, interpret partial success, compare ordered and unordered execution, and use modern cross-namespace bulk APIs without assuming all-or-nothing behavior.
Learning objectives
Inventory index definitions, sizes, usage counters, and query-plan evidence before changing index lifecycle state.
Use hidden indexes as a reversible query-planner experiment while remembering that hidden indexes remain fully maintained.
State correctly that MongoDB cannot rename an index in place and demonstrate the required drop/recreate workflow on a disposable index.
Retire an index only after query-shape evidence, node-aware usage observation, hide testing, and rollback criteria.
Measure write/storage overhead of unnecessary indexes without mistaking a micro-timing snippet for a benchmark.
This lesson pins MongoDB Community Server
8.3.8 with
mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim
and mongosh 2.10.0. The server is a disposable
standalone published only on loopback
127.0.0.1:27066. Authentication and TLS are
disabled only for this isolated lab. Feature Compatibility
Version (FCV) is observed but never changed. Default read/write
concern and primary read preference apply. Atlas, Search, KMS,
Enterprise Advanced, and paid services are not required. Runtime
output shown as “expected” is documentation-derived because this
generation environment has no Docker/mongod/mongosh runtime.
Index behavior depends on query shape, projection, sort, data
distribution, planner choice, cache state, topology, and patch
version. The labs therefore inspect winningPlan,
totalKeysExamined, totalDocsExamined,
index definitions/sizes, and target query results. Small
fixtures prove semantics, not production latency. Timing
snippets are comparative demonstrations only, not benchmarks.
1. Index lifecycle is an operational change, not housekeeping
Indexes are persisted, replicated data structures. A safe
lifecycle has evidence gates:
create → inspect → observe use → test visibility →
retire. The relevant evidence comes from getIndexes(),
indexSizes/totalIndexSize(), query
explains, and $indexStats. Usage count zero is a
clue, not automatic proof of uselessness: rare incident queries,
uniqueness, TTL, startup resets, hidden/unhidden resets, or
another replica member can explain it.
| Evidence | What it answers | Important limitation |
|---|---|---|
| getIndexes() | What definitions/options exist? | Does not tell you whether workload queries benefit. |
| indexSizes / totalIndexSize() | How much index storage is allocated? | Bytes vary with storage engine/compression/allocation. |
| $indexStats.accesses | How many user operations used an index on this node since its stats epoch? | Per-node; resets on restart, drop/recreate, and index modification. |
| explain("executionStats") | How did one query shape execute? | One query/input/cache state is not a workload trace. |
| hideIndex() | What happens when planner cannot select this index? | Index still consumes write, disk, and memory cost. |
2. Seed an index portfolio and capture the baseline
docker rm -f atlasmart-mongo-ch10-l5 2>/dev/null || truedocker volume rm atlasmart-mongo-ch10-l5-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch10-l5 \ -p 127.0.0.1:27066:27017 \ -v atlasmart-mongo-ch10-l5-data:/data/db \ mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27066/atlasmart?directConnection=true" --quiet --eval \'printjson({server:db.version(),hello:db.hello().isWritablePrimary}); printjson(db.getSiblingDB("admin").runCommand({getParameter:1,featureCompatibilityVersion:1}))'
const c=db.orders_ch10_l5;c.drop();const docs=[];for(let i=0;i<400;i++) docs.push({_id:i+1,tenantId:`tenant-${i%8}`,status:i%7===0?"cancelled":"paid",createdAt:new Date(Date.UTC(2026,7,1)+i*60000),totalCents:500+(i*97)%15000,customerId:`C-${i%70}`});c.insertMany(docs);c.createIndex({tenantId:1,status:1,createdAt:-1},{name:"idx_orders_workload"});c.createIndex({customerId:1},{name:"idx_customer_candidate"});c.createIndex({status:1},{name:"idx_status_candidate"});printjson(c.getIndexes());printjson({indexSizes:c.stats().indexSizes,totalIndexBytes:c.totalIndexSize()});
const c=db.orders_ch10_l5;for(let i=0;i<8;i++){ c.find({tenantId:`tenant-${i}`,status:"paid"}).sort({createdAt:-1}).limit(5).toArray();}c.find({customerId:"C-17"}).toArray();printjson(c.aggregate([{$indexStats:{}},{$project:{name:1,key:1,accesses:1,hidden:"$spec.hidden"}},{$sort:{name:1}}]).toArray());
$indexStats reports access metrics for the node where it runs.
A replica set or sharded cluster needs node-aware collection
of evidence. Its since field matters; an ops
count without its measurement epoch is incomplete evidence.
3. Hide before drop when you need a reversible planner test
Hiding removes an index from query-planner consideration while
keeping the structure fully updated. This makes unhide an
immediate rollback. The hidden index cannot be forced with
hint(). Hiding/unhiding also resets that index’s
$indexStats, so record the pre-change counters
first.
const c=db.orders_ch10_l5;const shape=()=>c.find({customerId:"C-17"},{_id:0,customerId:1,totalCents:1}).explain("executionStats");print("before hide"); printjson(shape());printjson(c.hideIndex("idx_customer_candidate"));printjson(c.getIndexes());print("after hide"); printjson(shape());try{ c.find({customerId:"C-17"}).hint("idx_customer_candidate").toArray(); }catch(e){ printjson({hiddenHintRejected:true,code:e.code,message:e.message}); }printjson(c.unhideIndex("idx_customer_candidate"));print("after unhide"); printjson(shape());
A hidden index still consumes disk/cache, is updated on writes, and if unique still enforces uniqueness. Hiding measures read-plan dependency, not the write savings you would get after a real drop.
4. “Rename” means drop and recreate—there is no rename-index command
MongoDB index names are immutable after creation. To change a name, snapshot the exact definition/options, drop the old index, and recreate the same key/options with the desired name. That operation resets statistics and can create a period where the access path or invariant is absent. Therefore an index rename is not cosmetic in production.
const c=db.orders_ch10_l5;const old=c.getIndexes().find(x=>x.name==="idx_status_candidate");printjson({oldDefinition:old});print("MongoDB has no in-place index rename command.");c.dropIndex("idx_status_candidate");print(c.createIndex({status:1},{name:"idx_status_v2"}));printjson(c.getIndexes().filter(x=>x.key.status===1));
If the index is unique or performance-critical, a drop/recreate name change can remove an invariant or access path during the build interval. If the name has no operational value, keeping the old name is often safer than rebuilding merely for aesthetics.
5. Evidence-driven retirement: observe → hide → drop
After a representative observation window, hide the candidate and verify application query plans, latency tails, CPU/I/O, and error rates. Only then drop it. The example uses the disposable status index; it does not claim that a short lab observation is equivalent to a production traffic window.
const c=db.orders_ch10_l5;print("candidate definitions before retirement");printjson(c.getIndexes().filter(x=>["idx_customer_candidate","idx_status_v2"].includes(x.name)));printjson(c.hideIndex("idx_status_v2"));print("observe application workload before permanent drop; hidden indexes still incur write cost");printjson(c.dropIndex("idx_status_v2"));printjson({remaining:c.getIndexes().map(x=>x.name),indexSizes:c.stats().indexSizes,totalIndexBytes:c.totalIndexSize()});
Before drop, rollback is unhideIndex() and is
immediate because the index stayed maintained. After drop,
rollback is a new index build with disk, CPU, replication, and
time cost. That difference is why hide-first is operationally
valuable.
6. Measure the cost of index explosion
The anti-pattern “index every field just in case” can degrade write-heavy workloads long before a hard index-count limit matters. The following isolated comparison writes identical documents to a lean collection and one with five secondary indexes. Interpret only the direction and inspect index bytes; repeat with production-like data if the decision matters.
const lean=db.orders_ch10_l5_lean;const heavy=db.orders_ch10_l5_heavy;lean.drop(); heavy.drop();heavy.createIndexes([{a:1},{b:1},{c:1},{a:1,b:1,c:1},{d:1,e:1}]);function docs(n){const a=[];for(let i=0;i<n;i++)a.push({_id:i,a:i%20,b:i%50,c:i%7,d:i%100,e:i%3});return a;}const batch=docs(5000);let t=Date.now(); lean.insertMany(batch); const leanMs=Date.now()-t;t=Date.now(); heavy.insertMany(batch); const heavyMs=Date.now()-t;printjson({leanMs,heavyMs,leanIndexBytes:lean.totalIndexSize(),heavyIndexBytes:heavy.totalIndexSize()});
Date.now() around one insert batch ignores
warm-up, cache state, checkpointing, fsync behavior,
concurrency, replication, and tail latency. Use workload
generators and production-like topology for performance
claims.
7. Build/drop operations need capacity and failure plans
Index builds on populated collections consume CPU, memory, disk, I/O, and replicated work. MongoDB 7.1+ improved index-build failure reporting and supports a minimum available disk-space guard; exact build behavior remains version/topology sensitive. Dropping a live needed index can cause immediate query regressions, while dropping an in-progress build can abort the build across replica members. Inspect active operations and capacity before treating build/drop as a routine migration.
Creating, hiding, modifying, and dropping indexes are administrative capabilities. Application runtime credentials should not casually possess index-management privileges. Search indexes are a separate subsystem and are not managed by these ordinary B-tree index methods.
8. Verification, cleanup, and production judgment
Verification checklist
- Index definitions and physical sizes are captured before lifecycle changes.
- $indexStats is read with its per-node and reset semantics documented.
- Hiding changes planner eligibility but does not stop maintenance.
- A hidden index cannot be hinted.
- The lesson uses drop/recreate—not a nonexistent rename command—to change an index name.
- Permanent drop occurs only after a hide/observation step on a disposable candidate.
- The over-indexed write test is explicitly treated as a demonstration rather than a benchmark.
Production judgment. The best index portfolio is the smallest set that protects required invariants and recurring query shapes at acceptable write/storage cost. Measure per-node use, query plans, tail latency, storage/cache pressure, and build/recovery headroom. Hide for reversible read-path testing; remember hiding never yields write savings. Treat drop/recreate as a real migration, especially for unique indexes. This chapter establishes the lifecycle foundation needed for Chapter 11’s specialized partial, sparse, TTL, wildcard, hashed, text, and geospatial index families.
docker rm -f atlasmart-mongo-ch10-l5docker volume rm atlasmart-mongo-ch10-l5-data
Check your understanding
- Why is $indexStats accesses.ops=0 not enough by itself to drop an index?
- What changes when an index is hidden?
- Does hiding reduce write amplification?
- How do you rename an ordinary MongoDB index?
- Why is unhide a cheaper rollback than recreating a dropped index?
Review the answers
1. Statistics are per-node and epoch-bound, and an index may serve rare queries or enforce properties not reflected by ordinary user-read counts.
2. The query planner cannot select it and it cannot be hinted, but the index remains present and maintained.
3. No. Hidden indexes are still updated on writes and consume storage/cache.
4. You cannot rename it in place; capture
the definition, drop it, and recreate it with a new
name.
5. The hidden index is already fully built and maintained, so unhiding makes it immediately planner-eligible; a dropped index must be rebuilt.
Authoritative references
- MongoDB 8.3 release notes — Current 8.3 baseline and patch-sensitive behavior; re-check before reproduction.
- Indexes overview — Index concepts, names, build considerations, and index-management overview.
- Explain results — IXSCAN/FETCH/COLLSCAN evidence, covered-query plans, keys/documents examined, and execution statistics.
- Measure index use — Using $indexStats and explain evidence instead of intuition to manage indexes.
- mongosh release notes — mongosh version used for chapter commands.
- Hidden indexes — Planner invisibility, continued maintenance/uniqueness, hint restriction, FCV requirement, and stats reset behavior.
- $indexStats — Per-node usage counters, measurement epoch, reset conditions, and index specs.
- Drop an index — Hide-first recommendation and drop procedures.
- Create indexes — Index options, hidden-option changes, build behavior, and current index-build considerations.