MongoDB / Basic: document foundations / Lesson 4
Filters, projection, sorting and pagination
State exactly which documents and fields a client should receive. Combine filters safely and choose stable cursors without assuming natural collection order.
On this page
Lesson goals
- Compose type-aware predicates and valid projections.
- Use deterministic sorting and bounded cursor reads.
Filter documents and project fields separately
A find filter selects documents; projection chooses the fields returned from each match. Projection is useful for payload control, but is not a substitute for authorization: an application still needs tenant and resource-scope predicates. MongoDB does not guarantee insertion order without a stated sort, and a limit alone does not make a chosen subset deterministic. Bind application inputs as validated values within a known query structure rather than accepting arbitrary client-supplied operator objects. A JSON-shaped query can still be an injection or scope-expansion risk.
Express the desired subset and output
Assume the synthetic collection contains parcel 1 pending with fee twenty, parcel 2 sent with fee fifty and parcel 3 pending with fee thirty, all numeric. This query returns parcel 3 then parcel 1 and only their identifiers and fees. _id is included deliberately as a stable tie-breaker. Projection inclusion normally cannot be mixed with exclusion of unrelated fields; excluding _id is the notable standard exception. Exact client formatting can vary, so the output states logical documents rather than a measured cursor transcript.
db.getSiblingDB('quay_lab').parcels.find(
{ state: 'pending', fee: { $gte: 20 } },
{ _id: 1, fee: 1 }
).sort({ fee: -1, _id: 1 });
{ _id: 3, fee: 30 }
{ _id: 1, fee: 20 }
Both predicates must match. The example assumes the stated numeric fixture only.
Write boolean and presence rules visibly
Fields in the same ordinary filter imply conjunction, while $or and $and express explicit alternatives or combinations. Repeating the same JavaScript object key can overwrite an earlier condition before the driver even sends it, so combine operators under one field or use a suitable $and structure. Missing fields and null equality need their own policy. Do not copy SQL NULL assumptions into MongoDB. Review negative predicates carefully because $ne and $nin often include a broad set and may be poor selective access paths.
Use a cursor matching the ordered key
For an increasing numeric _id training key, the next-page predicate below uses the last returned identifier rather than skipping an ever-growing offset. If production order uses createdAt and _id, the cursor must represent both components with the corresponding lexicographic condition. Concurrent inserts, updates and deletions can still change later pages unless the application defines a stronger snapshot contract. Choose that contract explicitly rather than presenting cursor pagination as a universal consistency guarantee. Limit returned data and close unused driver cursors promptly.
db.getSiblingDB('quay_lab').parcels.find(
{ _id: { $gt: 100 }, state: 'pending' },
{ _id: 1, state: 1 }
).sort({ _id: 1 }).limit(20);
The fixture must use increasing numeric identifiers for this exact cursor. ObjectId or compound orders require their own matching boundary.
Sorting obeys BSON and collation rules
MongoDB has a BSON comparison order across types, and array sorting has special semantics. It does not inherit PostgreSQL’s NULLS FIRST or NULLS LAST syntax. Prefer consistent field types and an explicit application policy for missing values rather than relying on mixed-type order for business meaning. A matching index can support filtering and sorting, but limit bounds returned documents rather than all examined work. Inspect a plan after correctness is established, and avoid broad unanchored regular expressions as an assumed efficient search solution.
Common errors and fixes
A filter silently loses one repeated field condition
- Why it happens
- Duplicate JavaScript keys overwrote an earlier value.
- How to fix it
- Combine field operators or use a deliberate $and array.
- Prevent it next time
- Inspect the actual constructed filter, not only source-line intent.
Projection reports an inclusion/exclusion error
- Why it happens
- The projection mixed incompatible rules outside the _id exception.
- How to fix it
- Choose one projection mode and handle _id explicitly.
- Prevent it next time
- Test response shape independently from matching logic.
Pages repeat tied documents
- Why it happens
- Sort order or cursor boundaries omit a stable unique component.
- How to fix it
- Use a total order and a cursor containing every ordered component.
- Prevent it next time
- Test ties and concurrent data changes.
Check your understanding
1. A filter object declares fee twice, once $gte and once $lte. Why can it fail before MongoDB evaluates it?
Show worked solution
JavaScript object construction normally retains only the last occurrence of a duplicate key, so one bound can disappear. Use { fee: { $gte: 10, $lte: 20 } } for a range on one field, or a valid explicit conjunction when necessary. Validate the final serialized query shape in application tests rather than assuming both source lines survive.
2. The response needs label and city but not _id. What projection mode should be used?
Show worked solution
Use inclusion for the required fields with _id: 0, for example { label: 1, "destination.city": 1, _id: 0 }. This is the normal _id exception to mixing projection modes. Confirm the embedded output shape and keep access-scope filters separate; hiding an identifier does not authorize exposing another tenant’s label or city.
Key takeaways
- Query structure, projection and ordering are separate contracts.
- Stable cursors must match BSON types and every ordered component.