Skip to main content
MongoDB · lesson 1 of 10
MongoDB beginner Lesson 1 of 10

MongoDB 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

mongodb mongosh docker crud getting-started

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.

Frequently Asked Questions

Why does this track skip deep beginner CRUD coverage?
MongoDB University's own free M001 course, plus w3schools and tutorialspoint, already cover insert/find/update/delete thoroughly and well. This track's real ground is what those sources treat lightly or not at all: measured index/aggregation performance, a real replica-set failover timed in seconds, and production schema tradeoffs — see research/mongodb.md for the full comparison.
Why did querying price as a string return nothing, even though the field clearly has that value?
MongoDB query matching compares by both value and BSON type — 9.99 (a double) and "9.99" (a string) are different types, so { price: "9.99" } never matches a document where price is stored as a number. There's no automatic type coercion in query matching, unlike some SQL engines that implicitly cast.
What does { title: 1, price: 1, _id: 0 } in a find() call do?
It's a projection — the second argument to find() (or the only argument to findOne() beyond the filter) that controls which fields come back. 1 means include, 0 means exclude; _id is included by default unless explicitly excluded, which is why _id: 0 is needed here to leave it out.
Is a Docker container the same as a real production MongoDB deployment?
For a single node running the same MongoDB Community Server binary, functionally yes — every command in this track runs exactly as it would against a real deployment. What a single container can't demonstrate on its own is a multi-node replica set or a multi-shard cluster, which is why later lessons in this track spin up additional containers rather than relying on just this one.
What does the acknowledged field in an update/delete result actually mean?
It reflects whether the write concern was satisfied — acknowledged: true means the server confirmed the operation completed under the requested write concern (the default acks from the primary). It says nothing about whether anything actually matched or changed; matchedCount and modifiedCount (or deletedCount) are the fields that answer that.