Chapter 03 · CRUD Fundamentals: Insert, Find, Projection, Sort, Limit, and Delete
Find Filters, Comparison/Logical Operators, Projection, Sorting, Skipping, and Limiting
Turn find into an explicit query contract: predicate, returned fields, total sort order, offset, and bound—then verify exactly what the server returned.
Learning outcomes
AtlasMart's catalog API must answer “active cameras between two
prices, show only public fields, cheapest first, page two”
without pulling the whole collection into application memory. A
correct find() request is a contract made of a
filter, projection,
sort, skip, and
limit. If any piece is vague, pagination and
API responses become nondeterministic or wasteful.
Build precise find filters with equality, comparison, and logical operators.
Distinguish inclusion and exclusion projection rules, including the special _id exception.
Create deterministic sort orders by adding a unique tie-breaker.
Use skip and limit correctly while recognizing that offset pagination has scaling tradeoffs.
Verify server-side filtering and returned document shape rather than filtering in Python after an unbounded read.
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.
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. The filter defines the candidate set
A query filter is a BSON document that
describes which server-side documents match. Equality is
implicit: {category:"camera"}. Comparison operators
such as $gte and $lt constrain ranges.
Logical operators such as $and and
$or combine predicates, although multiple fields in
one filter are already an implicit AND. Chapter 04 will go
deeper into arrays, nested fields, null/missing semantics, and
expressions; here the goal is disciplined predicate
construction.
const filter = { category: "camera", active: true, priceCents: {$gte: 5000, $lt: 20000}, $or: [{stock: {$gte: 5}}, {backorderAllowed: true}]};printjson(filter);printjson(db.crud_products.find(filter).sort({_id:1}).toArray());
The filter belongs on the server because MongoDB can use indexes
and avoid transmitting rejected documents. A pattern such as
list(coll.find({})) followed by Python
if statements is client-side filtering: it consumes
network, application memory, and server work for rows the API
never needed.
2. Projection shapes the response but does not change stored documents
A projection controls returned fields.
Inclusion projection such as
{sku:1,name:1,priceCents:1,_id:0} returns only
those fields and suppresses the otherwise included
_id. Exclusion projection such as
{internalNotes:0} returns all fields except the
named field. You generally cannot mix inclusion and exclusion in
the same projection, except for the special handling of
_id.
const filter = {category:"camera", active:true};const projection = {_id:0, sku:1, name:1, priceCents:1};printjson(db.crud_products.find(filter, projection).sort({priceCents:1, _id:1}).toArray());// Deliberately wrong: mixed include/exclude (other than _id) should fail.try { db.crud_products.find({}, {name:1, internalNotes:0}).toArray();} catch (e) { print("projection error:", e.message); }
Projection reduces response size and can reduce unnecessary data exposure, but it is not an authorization boundary. A caller who is allowed to issue another query may still retrieve fields unless application/database privileges prevent it. Do not mistake “this endpoint projects out internalNotes” for tenant isolation.
3. Sorting must include a unique tie-breaker when order matters
MongoDB collections do not have an application-defined default
order. Sorting by a field with duplicates—such as
priceCents—does not guarantee a stable relative
order among equal prices. If pagination or tests require
reproducibility, include a unique field such as
_id as the final sort key.
// Unsafe for repeatable pagination when many documents share priceCents.db.crud_products.find({active:true}).sort({priceCents:1}).limit(4)// Deterministic tie-breaker.db.crud_products.find({active:true}) .sort({priceCents:1, _id:1}) .limit(4)
The unique tie-breaker determines a total order for this dataset. It does not freeze the collection against concurrent inserts/updates between page requests. Stable application pagination across changing data may need a snapshot/transaction or a range-based continuation token designed around immutable sort keys.
4. skip and limit define a window, not a durable page identity
skip(n) discards the first n sorted
matches and limit(m) caps the number returned.
Chaining order does not change the logical result: MongoDB
applies the sort/window semantics appropriately. When
skip() is used with sorting, include a unique sort
field to avoid inconsistent results for duplicate sort keys.
const filter = {active:true};const projection = {_id:1, sku:1, priceCents:1};const sort = {priceCents:1, _id:1};const pageSize = 3;const page2 = db.crud_products.find(filter, projection) .sort(sort) .skip(pageSize) .limit(pageSize) .toArray();printjson(page2);
Offset pagination is understandable and useful for small result sets, but large skips can become expensive because the server still has to advance past earlier results. Later indexing/query-planning chapters will measure that cost and introduce range/seek patterns when appropriate.
5. AtlasMart query lab: record the exact contract
docker rm -f atlasmart-mongo-ch03-l2docker run --name atlasmart-mongo-ch03-l2 -p 127.0.0.1:27028:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimdocker logs atlasmart-mongo-ch03-l2 --tail 25
mongosh "mongodb://127.0.0.1:27028/atlasmart?directConnection=true" --quiet --eval 'db.crud_products.drop();db.crud_products.insertMany([ {_id:"p1",sku:"cam-a",name:"A Cam",category:"camera",priceCents:9000,stock:2,backorderAllowed:true,active:true,internalNotes:"n1"}, {_id:"p2",sku:"cam-b",name:"B Cam",category:"camera",priceCents:9000,stock:9,backorderAllowed:false,active:true,internalNotes:"n2"}, {_id:"p3",sku:"cam-c",name:"C Cam",category:"camera",priceCents:12000,stock:6,backorderAllowed:false,active:true,internalNotes:"n3"}, {_id:"p4",sku:"cam-d",name:"D Cam",category:"camera",priceCents:15000,stock:0,backorderAllowed:false,active:true,internalNotes:"n4"}, {_id:"p5",sku:"bag-a",name:"A Bag",category:"bag",priceCents:5000,stock:20,backorderAllowed:false,active:true,internalNotes:"n5"}, {_id:"p6",sku:"cam-e",name:"E Cam",category:"camera",priceCents:18000,stock:7,backorderAllowed:false,active:false,internalNotes:"n6"}]);const filter={category:"camera",active:true,priceCents:{$gte:8000,$lt:16000},$or:[{stock:{$gte:5}},{backorderAllowed:true}]};const projection={_id:1,sku:1,name:1,priceCents:1};const docs=db.crud_products.find(filter,projection).sort({priceCents:1,_id:1}).skip(0).limit(10).toArray();printjson({filter, projection, returned:docs.length, docs});'
Verification checklist
- The printed filter and projection are visible next to the result rather than implied.
- Only active cameras in the requested price range and stock/backorder condition are returned.
-
internalNotesis absent from projected output. -
Equal prices are ordered by the unique
_idtie-breaker. - The returned count describes this result set; it is not confused with collection size.
Check your understanding
- Why is sort({priceCents:1}) insufficient for deterministic pagination?
- Can projection mix name:1 and internalNotes:0?
- Why is client-side filtering usually a bad default?
- Does a projection secure a hidden field?
- What does skip(30).limit(10) mean?
Review the answers
Multiple documents can have the same price, and MongoDB does not guarantee a stable relative order for equal sort keys. Add a unique tie-breaker such as _id.
Not generally. A projection is inclusion or exclusion, with _id as the special exception.
It transfers and materializes documents the server could reject, increasing network, memory, and work and bypassing potential index use.
No. Projection shapes one query result; authorization must be enforced through application/database security boundaries.
After applying the filter and sort, advance past 30 matches and return at most the next 10; it does not identify an immutable page under concurrent changes.
docker rm -f atlasmart-mongo-ch03-l2
The next lesson opens the black box behind find():
cursors, batches, getMore, exhaustion, and client cleanup.
Authoritative references
- MongoDB release notes — Official current stable server series and patch notes.
- MongoDB 8.3 release notes — Official 8.3 patch history; 8.3.8 is the latest released patch at review time.
- MongoDB CRUD operations — Official CRUD overview and server semantics.
- PyMongo CRUD guides — Official Python driver CRUD behavior and result objects.
- find() — Filter, projection, sort behavior, and mongosh cursor handling.
- cursor.sort() — Sort consistency and unique tie-breakers.
- cursor.skip() — Skip semantics and stable-sort warning.
- PyMongo specify documents to return — Official limit/sort/skip driver usage.