Chapter 03 · CRUD Fundamentals: Insert, Find, Projection, Sort, Limit, and Delete

DeleteOne/DeleteMany, Precise Predicates, Safety Checks, and Observable Change Counts

Make deletion a reviewed state transition: preview exact scope, use precise predicates, inspect acknowledged/deleted counts, and prove the postcondition.

Beginner100–125 minutesScoped delete + before/after verification labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning outcomes

AtlasMart must purge expired import-staging records without touching current products. Deletes are easy to type and hard to undo. The correct workflow is therefore preview the exact predicate → verify scope → delete → inspect acknowledged/deleted counts → query the same predicate again. The destructive command is only one step in the evidence chain.

01

Explain deleteOne versus deleteMany and their acknowledged/deletedCount result metadata.

02

Use unique or otherwise precise predicates when the business intent is one specific document.

03

Build preflight count/sample checks before deleteMany and verify the postcondition afterward.

04

Diagnose the danger of empty or broad filters and of assuming deleteOne chooses a deterministic match.

05

Understand topology caveats: multi-shard deleteMany and concurrent chunk migrations are separate concerns from this standalone lab.

Chapter 03 reproducible baseline

Mandatory labs use a disposable loopback-only standalone mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim with a dedicated AtlasMart collection and explicit reset commands. Driver examples pin pymongo==4.17.0. The standalone is intentionally unauthenticated only for these short-lived local exercises; do not publish it beyond 127.0.0.1. Docker commands use syntax accepted directly by ordinary shells; if the first cleanup reports that the container does not exist, that is harmless. Retryable-write behavior that requires a replica set or sharded cluster is explained but not claimed for this standalone.

Generation-time execution note

Docker, mongod, mongosh, and PyMongo are not available in this generation environment. Commands were checked against current official MongoDB Server and PyMongo documentation, but product commands were not executed here. Expected-output blocks describe stable fields and relationships to verify; they are not fabricated captured transcripts.

1. deleteOne removes at most one match; it does not express which arbitrary match you intended

deleteOne(filter, options) removes one matching document and returns {acknowledged, deletedCount}. If the filter matches nothing, deletedCount is zero. If the filter can match several documents, using deleteOne is usually a modeling/API smell because the application has not identified the target precisely. Prefer a unique key such as _id or another uniquely indexed business identifier when the intent is one specific record.

mongosh · precise single-document delete
const target = db.crud_products.findOne({_id:"retired-001"}, {_id:1,sku:1,status:1});printjson({before:target});const result = db.crud_products.deleteOne({_id:"retired-001"});printjson(result);printjson({after:db.crud_products.findOne({_id:"retired-001"})});

deletedCount:1 plus a subsequent null lookup proves this document is absent after the operation in the observed state. It does not prove an external system cannot recreate it later or that a replica/backup retention requirement is satisfied.

2. deleteMany should begin as a read-only preflight

deleteMany(filter) removes all matching documents. Before running it, execute the same filter through countDocuments() and a bounded sorted sample. That turns a destructive intention into a reviewable scope.

mongosh · preflight, bounded delete, postcondition
const filter = {source:"import-2026-09-02", status:"staging", expiresAt:{$lt:ISODate("2026-09-02T00:00:00Z")}};const count = db.crud_products.countDocuments(filter);const sample = db.crud_products.find(filter, {_id:1,sku:1,expiresAt:1}).sort({_id:1}).limit(10).toArray();printjson({filter, count, sample});if (count > 0 && count <= 100) {  const result = db.crud_products.deleteMany(filter);  printjson(result);  printjson({remaining:db.crud_products.countDocuments(filter)});} else {  print("ABORT: preflight count outside expected lab range");}

The numerical guard is a lab-specific safety belt, not a production universal threshold. Real systems should derive thresholds from expected batch size, approval policy, maintenance windows, tenant scope, and recovery capability.

3. Deliberately wrong: empty and under-scoped predicates

deleteMany({}) matches every document in a collection. It does not drop the collection or its indexes, but it still destroys all records. Another dangerous filter is syntactically valid but missing a tenant or batch boundary, for example {status:"staging"} when the intention was only one import job.

mongosh · expose under-scoped delete before it happens
// DO NOT run against valuable data.const dangerous = {status:"staging"};const intended = {tenantId:"tenant-demo", source:"import-2026-09-02", status:"staging"};printjson({dangerousCount:db.crud_products.countDocuments(dangerous)});printjson({intendedCount:db.crud_products.countDocuments(intended)});printjson(db.crud_products.find(dangerous,{_id:1,tenantId:1,source:1,status:1}).sort({_id:1}).limit(20).toArray());

The repair is not merely “be careful.” Encode tenant/import identity in the filter, log the exact predicate, compare expected versus actual count, require approval for unusual scope, and maintain tested backups/restore procedures. Destructive correctness is a workflow property.

4. Driver results and unacknowledged-write limits

PyMongo returns a DeleteResult. With acknowledged writes you can inspect deleted_count and raw_result. If a write is explicitly unacknowledged, the driver cannot truthfully provide those counts; accessing result properties other than acknowledged is invalid. This is why safety-critical deletion should normally use acknowledged write concern.

Python · DeleteResult and postcondition
from pymongo import MongoClientclient = MongoClient("mongodb://127.0.0.1:27030/?directConnection=true", serverSelectionTimeoutMS=3000)coll = client.atlasmart.crud_productsflt = {"tenantId":"tenant-demo", "source":"import-2026-09-02", "status":"staging"}print("preflight", coll.count_documents(flt))result = coll.delete_many(flt)print("acknowledged", result.acknowledged)print("deleted_count", result.deleted_count)print("remaining", coll.count_documents(flt))client.close()

In sharded deployments, deleteMany() has additional routing and migration caveats. Current MongoDB documentation warns that concurrent chunk migrations can cause a multi-document delete to miss matching documents, with recommended mitigation such as iterative deletion until the same query returns none or using a transaction where appropriate. That behavior is not reproduced by this standalone lesson; it belongs to later sharding/transaction chapters.

5. AtlasMart safe-delete lab

shell · start disposable MongoDB on 127.0.0.1:27030
docker rm -f atlasmart-mongo-ch03-l4docker run --name atlasmart-mongo-ch03-l4 -p 127.0.0.1:27030:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimdocker logs atlasmart-mongo-ch03-l4 --tail 25
shell · prove scoped deletion
mongosh "mongodb://127.0.0.1:27030/atlasmart?directConnection=true" --quiet --eval 'db.crud_products.drop();db.crud_products.insertMany([ {_id:"live-1",tenantId:"tenant-demo",source:"catalog",status:"active"}, {_id:"stage-1",tenantId:"tenant-demo",source:"import-2026-09-02",status:"staging"}, {_id:"stage-2",tenantId:"tenant-demo",source:"import-2026-09-02",status:"staging"}, {_id:"other-stage",tenantId:"tenant-other",source:"import-2026-09-02",status:"staging"}]);const filter={tenantId:"tenant-demo",source:"import-2026-09-02",status:"staging"};printjson({beforeCount:db.crud_products.countDocuments(filter), sample:db.crud_products.find(filter).sort({_id:1}).toArray()});const r=db.crud_products.deleteMany(filter);printjson(r);printjson({remainingSameFilter:db.crud_products.countDocuments(filter), survivors:db.crud_products.find({}, {_id:1,tenantId:1,status:1}).sort({_id:1}).toArray()});' 

Verification checklist

  • The preflight finds exactly the two intended tenant-demo staging records.
  • deletedCount equals the preflight count in this quiescent standalone fixture.
  • A postcondition query using the identical filter returns zero.
  • The active document and the other tenant's staging record remain.
  • You can explain why this equality is not automatically guaranteed in every concurrent sharded scenario.

Check your understanding

  1. Why is {_id:value} a safer deleteOne filter than {status:"retired"}?
  2. What should happen before deleteMany?
  3. What does deletedCount:0 mean?
  4. Why is deleteMany({}) dangerous even though it keeps indexes?
  5. Why are sharded deleteMany caveats deferred from the lab?
Review the answers

_id is unique, so the filter identifies the intended document rather than an arbitrary member of a larger matching set.

Run the exact filter as a read-only preflight: count it, inspect a bounded deterministic sample, and compare scope with expectations.

No document matching that operation was deleted. It may mean the filter matched none, or the target state changed before execution.

It matches every document, so it removes all collection records. Preserving collection metadata does not make data loss safe.

They require a sharded topology and chunk-migration behavior; the standalone lab cannot honestly demonstrate those conditions.

bash · cleanup/reset
docker rm -f atlasmart-mongo-ch03-l4

The final lesson combines insert, find, cursor, and delete into one auditable CRUD workflow.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.