MongoDB · lesson 3 of 10
Intermediate
6- 2 Embedding vs Referencing: A 5.9MB Document and a 50x Fetch Time
- 3 explain() Proves It: 200,000 Docs Scanned vs. a Covered Query That Touches Zero
- 4 The Aggregation Stage-Order Bug: Wrong Results, No Error
- 5 When the Aggregation Optimizer Can (and Can't) Use Your Index
- 6 Change Streams: Watching a Collection in Real Time (Replica Set Required)
- 7 Multi-Document Transactions: When One Update Isn't Enough
Advanced
3Also part of MongoDB
1explain() Proves It: 200,000 Docs Scanned vs. a Covered Query That Touches Zero
A query against 200,000 documents takes 65ms and scans every single one without an index. One index later it's 2ms. A covered query on top of that touches zero documents at all — explain() proves each claim.
By SkillByExample · Published
The same 200,000-document collection, queried three different ways — a full scan, an indexed lookup, and a query that never touches a document at all.
Seeding a realistically sized collection
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
const genres = ["scifi", "fantasy", "mystery", "romance", "history", "biography"];
const bulk = [];
for (let i = 0; i < 200000; i++) {
bulk.push({
title: "Book " + i,
author: "Author " + (i % 5000),
genre: genres[i % genres.length],
price: 5 + (i % 50),
published_year: 1950 + (i % 75)
});
if (bulk.length === 20000) { db.books.insertMany(bulk, { ordered: false }); bulk.length = 0; }
}
if (bulk.length) db.books.insertMany(bulk, { ordered: false });
print("total:", db.books.countDocuments());
'
total: 200000
200,000 books, 5,000 distinct authors — enough that a full scan has real, measurable cost, not a rounding error.
The bug: a query with no index to use
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
const result = db.books.find({ author: "Author 4999" }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
print("docsExamined:", result.executionStats.totalDocsExamined);
print("nReturned:", result.executionStats.nReturned);
print("executionTimeMillis:", result.executionStats.executionTimeMillis);
'
stage: COLLSCAN
docsExamined: 200000
nReturned: 40
executionTimeMillis: 65
COLLSCAN means exactly what it sounds like: every one of the 200,000 documents was examined
to find the 40 that actually matched. docsExamined: 200000 next to nReturned: 40 is the
smoking gun — a query this selective (0.02% of the collection) had no business reading the
whole thing.
The fix: an index on the filtered field
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
db.books.createIndex({ author: 1 });
const result = db.books.find({ author: "Author 4999" }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
print("docsExamined:", result.executionStats.totalDocsExamined);
print("executionTimeMillis:", result.executionStats.executionTimeMillis);
'
stage: FETCH
docsExamined: 40
executionTimeMillis: 2
docsExamined dropped from 200,000 to exactly 40 — the number of documents that actually
matched, nothing more. Execution time went from 65ms to 2ms, over 30x faster. The top-level
stage is FETCH, not IXSCAN directly — the index (IXSCAN, visible as FETCH’s child
stage) narrows down which documents to read, and FETCH is the step that actually reads
them.
Compound indexes: order matters
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
db.books.createIndex({ genre: 1, price: 1 });
const result = db.books.find({ genre: "scifi", price: { $gte: 40 } }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
print("docsExamined:", result.executionStats.totalDocsExamined);
print("executionTimeMillis:", result.executionStats.executionTimeMillis);
'
stage: FETCH
docsExamined: 9334
executionTimeMillis: 10
{ genre: 1, price: 1 } supports filtering on genre alone, or genre and price
together, efficiently — index entries sort by genre first, price second within each genre,
the same left-to-right rule SQL composite indexes follow. A query filtering on price alone
wouldn’t use this index efficiently at all.
The covered query: zero documents touched
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
const result = db.books.find({ genre: "scifi", price: { $gte: 40 } }, { genre: 1, price: 1, _id: 0 }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
print("docsExamined:", result.executionStats.totalDocsExamined);
print("executionTimeMillis:", result.executionStats.executionTimeMillis);
'
stage: PROJECTION_COVERED
docsExamined: 0
executionTimeMillis: 4
docsExamined: 0. Both the filter fields (genre, price) and the projected fields (genre,
price, with _id explicitly excluded) are entirely satisfied by the genre_1_price_1
index — no document ever needs to be read from the collection at all. This is the fastest a
query can possibly be: the answer lives entirely in the index.
Practice
1. Query a field with no index and confirm it's still a full scan.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
const result = db.books.find({ published_year: 2000 }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
print("docsExamined:", result.executionStats.totalDocsExamined);
'
stage: COLLSCAN
docsExamined: 200000
published_year has no index of its own — genre_1_price_1 and author_1 don’t help here at
all, since neither includes published_year. Same full-collection cost as the very first
example.
2. List every index currently on the collection.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.getIndexes().map(idx => idx.name));
'
[ '_id_', 'author_1', 'genre_1_price_1' ]
Every collection gets the _id_ index automatically and for free — author_1 and
genre_1_price_1 are the two created earlier in this lesson. getIndexes() is the direct way
to audit what actually exists, rather than trying to remember what was created across a long
session.
3. Predict, then confirm, what happens querying by price alone against the genre-price compound index.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
const result = db.books.find({ price: { $gte: 40 } }).explain("executionStats");
print("stage:", result.executionStats.executionStages.stage);
'
stage: COLLSCAN
Confirms the left-to-right rule from earlier: { genre: 1, price: 1 } can’t efficiently serve
a query that filters on price without also filtering on genre — MongoDB falls back to a
full scan rather than using an index it can’t apply the way the query needs.
Next: aggregation pipelines — and the stage-order bug that returns wrong results with no error.