MongoDB · lesson 1 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
1MongoDB CRUD and the Type-Strict Query Trap
Run a real MongoDB server in Docker, do the four basic operations, then hit the surprise: querying a numeric field with a string never matches — MongoDB doesn't coerce types in queries.
By SkillByExample · Published
A MongoDB server is one Docker command away, and the first real surprise arrives on the very first query.
Running a real MongoDB server
docker run -d --name mongo-lesson -p 27017:27017 mongo:8
docker exec mongo-lesson mongosh --eval "db.runCommand({ping: 1})" --quiet
{ ok: 1 }
-p 27017:27017 publishes MongoDB’s default port; mongosh is the shell that ships inside
the same container. Every lesson in this track assumes this container is running, and uses a
bookshop database throughout.
Insert and find
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
db.books.insertMany([
{ title: "Dune", author: "Frank Herbert", genre: "scifi", price: 12.99, published_year: 1965 },
{ title: "Foundation", author: "Isaac Asimov", genre: "scifi", price: 9.99, published_year: 1951 }
]);
db.books.find({ genre: "scifi" }, { title: 1, price: 1, _id: 0 });
'
[
{ title: 'Dune', price: 12.99 },
{ title: 'Foundation', price: 9.99 }
]
db.getSiblingDB("bookshop") switches to (and implicitly creates, on first write) a database
named bookshop — no separate CREATE DATABASE step exists in MongoDB. The second argument
to find() is a projection: { title: 1, price: 1, _id: 0 } returns only those fields.
The bug: a query that should match, and doesn’t
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.find({ price: "9.99" }).toArray());
'
[]
Foundation clearly has price: 9.99 — the previous query returned it. This one, filtering
on price: "9.99" (a string), matches nothing at all.
Why: MongoDB doesn’t coerce types in query matching
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.find({ price: 9.99 }).toArray());
'
[
{
_id: ObjectId('6aa6c104e4506f0e65d6ba5c'),
title: 'Foundation',
author: 'Isaac Asimov',
genre: 'scifi',
price: 9.99,
published_year: 1951
}
]
Same value, correct BSON type (a number, not a string), and it matches immediately. A query
filter compares both value and type — 9.99 and "9.99" are never equal to MongoDB, no
matter how a SQL background might expect implicit casting. This is the single most common
“why isn’t my query matching” bug for anyone new to MongoDB coming from a more permissive
query language.
Update and delete
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.updateOne({ title: "Dune" }, { $set: { price: 14.99 } }));
printjson(db.books.findOne({ title: "Dune" }, { title: 1, price: 1, _id: 0 }));
'
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
title: 'Dune',
price: 14.99
}
updateOne needs $set to update a field in place — passing { price: 14.99 } directly as
the second argument would try to replace the entire document with just that one field, which
is almost never what’s intended. matchedCount and modifiedCount both being 1 confirms
exactly one document was found and actually changed.
Practice
1. Delete a document and confirm the collection's count afterward.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.deleteOne({ title: "Foundation" }));
print(db.books.countDocuments());
'
{ acknowledged: true, deletedCount: 1 }
1
deletedCount confirms exactly one document was removed; countDocuments() confirms the
collection now has one book left (Dune).
2. Try updating a document that doesn't match anything, and read the result.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.updateOne({ title: "Nonexistent" }, { $set: { price: 1 } }));
'
{
acknowledged: true,
insertedId: null,
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 0
}
No error — updateOne on a filter that matches nothing just reports matchedCount: 0.
Code that assumes a write “worked” because it didn’t throw needs to check this field
explicitly.
3. Use upsert: true to insert a document only if the filter doesn't match one.
docker exec mongo-lesson mongosh --quiet --eval '
db = db.getSiblingDB("bookshop");
printjson(db.books.updateOne(
{ title: "Snow Crash" },
{ $set: { author: "Neal Stephenson", price: 10.99 } },
{ upsert: true }
));
'
{
acknowledged: true,
insertedId: ObjectId('6aa6c1e1e4506f0e65d6ba60'),
matchedCount: 0,
modifiedCount: 0,
upsertedCount: 1
}
upsertedCount: 1 and a real insertedId confirm a new document was created because nothing
matched the filter — the same “update or create” pattern SQL’s INSERT ... ON CONFLICT or
MERGE solves, in one call.
Next: schema design — embedding vs. referencing, and the unbounded-array failure mode.