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.

Beginner100–125 minutesFilter/projection/sort deterministic-read labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

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.

01

Build precise find filters with equality, comparison, and logical operators.

02

Distinguish inclusion and exclusion projection rules, including the special _id exception.

03

Create deterministic sort orders by adding a unique tie-breaker.

04

Use skip and limit correctly while recognizing that offset pagination has scaling tradeoffs.

05

Verify server-side filtering and returned document shape rather than filtering in Python after an unbounded read.

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. 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.

mongosh · explicit server-side filter
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.

mongosh · projection and a deliberate rule violation
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.

mongosh · unstable vs deterministic sort
// 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.

mongosh · deterministic offset page
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

shell · start disposable MongoDB on 127.0.0.1:27028
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
shell · deterministic filter/projection/sort lab
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.
  • internalNotes is absent from projected output.
  • Equal prices are ordered by the unique _id tie-breaker.
  • The returned count describes this result set; it is not confused with collection size.

Check your understanding

  1. Why is sort({priceCents:1}) insufficient for deterministic pagination?
  2. Can projection mix name:1 and internalNotes:0?
  3. Why is client-side filtering usually a bad default?
  4. Does a projection secure a hidden field?
  5. 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.

bash · cleanup/reset
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

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.