How do the $slice and $elemMatch projection operators limit which array elements are returned?
answer
- One takes positions, the other takes a condition
- Negative counts read from the end
- Only one element ever comes back from the condition form
- [skip, limit] is a legal argument shape
- Need all matches? that is $filter in aggregation
basics
~20 s$slice returns a positional window of an array: a count from the front, a negative count from the back, or a [skip, limit] pair. $elemMatch returns only the first array element matching a condition, dropping the field entirely when nothing matches.
solid answer
~40 sBoth trim an array *in the returned document*; neither changes which documents match. `{comments: {$slice: 5}}` returns the first five elements, `{$slice: -5}` the last five, and `{$slice: [20, 10]}` skips 20 and returns 10 — a cheap way to page a bounded embedded array. `{$elemMatch: {...}}` in a projection is condition-based: it returns a one-element array holding the **first** element that satisfies the condition, and omits the field if no element does. The positional `$` operator does something similar but reuses the query's own condition on that array, so it requires the array field to appear in the filter. Remember the first-match-only limit: if you need every matching element, you need `$filter` in an aggregation, not a find projection.
code
javascript · 4 lines// positional windows over a bounded embedded array
db.posts.find({}, { comments: { $slice: 5 } }) // first 5
db.posts.find({}, { comments: { $slice: -5 } }) // last 5
db.posts.find({}, { comments: { $slice: [20, 10] } }) // skip 20, take 10go deeper
Learn the three $slice forms — count from the front, negative count from the back, and the [skip, limit] pair — and be able to write one that returns the last five elements of an embedded array.
Explain that a projection reshapes results without changing which documents match, and state the first-match-only rule for both $elemMatch projection and the positional $ operator.
Show the failure you have actually fixed in review: someone expected a filtered array from $elemMatch and shipped only the first hit. Reach for $filter, and treat slicing a huge embedded array as a modelling problem.
Own the underlying question: an array that needs server-side paging is often an aggregate boundary drawn too wide. Decide whether the array stays bounded in the document or becomes its own collection before optimizing the projection.
## The problem these solve A document may embed a large array — a product's reviews, a thread's comments, a device's readings. Returning the whole array to show three of its elements wastes bandwidth and client memory, and can push a document uncomfortably close to the 16 MB BSON limit when several such documents come back at once. MongoDB's `find()` projection therefore offers array-shaping operators that return a subset of an array's elements. The crucial framing: these are **projection** operators. They change the shape of what comes back; they never change which documents match. Selecting documents is the filter's job. ## $slice — positional windows `$slice` takes elements by position, ignoring their content, in three forms: ```javascript db.posts.find({}, { comments: { $slice: 5 } }) // first 5 db.posts.find({}, { comments: { $slice: -5 } }) // last 5 db.posts.find({}, { comments: { $slice: [20, 10] } }) // skip 20, take 10 ``` A positive number takes from the front, a negative number from the back. The two-element array form is `[skipCount, limitCount]`; a negative skip counts back from the end of the array. If the array is shorter than requested, you get what exists rather than an error, and if the field is not an array, `$slice` leaves it alone. This makes `$slice` the natural tool for paging a *bounded* embedded array — the latest few comments on a post, the last ten status transitions on an order. It is not a substitute for a real collection: the whole document is still read from storage and the whole array still lives in memory server-side, so slicing a 100,000-element embedded array is a design smell rather than an optimization. Bounded arrays are the precondition. ## $elemMatch in a projection — condition-based, first match only ```javascript db.students.find( { "grades.subject": "math" }, { name: 1, grades: { $elemMatch: { subject: "math" } } } ) ``` The returned `grades` is a one-element array containing the **first** element (in stored array order) whose sub-document matches `{subject: "math"}`. If the document has three math grades, you still get one. If no element matches, the `grades` field is omitted from that result document entirely — client code must handle its absence, not just an empty array. It is worth separating the two `$elemMatch` roles that share a name. In a **filter**, `$elemMatch` asks whether a single array element satisfies several conditions at once, and it selects whole documents. In a **projection**, it selects which element of an already-matched document to return. They are frequently used together, and their conditions do not have to be the same. ## The positional $ projection ```javascript db.students.find( { "grades.subject": "math" }, { name: 1, "grades.$": 1 } ) ``` The positional `$` returns the first array element that matched the **query's** condition on that array. It therefore requires the array field to appear in the filter, and it cannot express a condition of its own. `$elemMatch` is the more flexible of the two because its condition is independent — you can match documents on one criterion and pull back an element chosen by another. Both share the first-match-only limitation, and both return at most one element. ## When one element is not enough If you need *all* matching elements, no `find()` projection will do it. Use the aggregation framework's `$filter` expression: ```javascript db.students.aggregate([ { $match: { "grades.subject": "math" } }, { $project: { name: 1, grades: { $filter: { input: "$grades", as: "g", cond: { $eq: ["$$g.subject", "math"] } } } } } ]) ``` This is the single most common correction to make in a code review of array projections: someone reached for `$elemMatch` expecting a filtered array and silently shipped only the first hit. ## Interaction with the projection mode rule A projection is either inclusion or exclusion, and the array operators sit most naturally inside an inclusion projection alongside plain `1`s. `{name: 1, grades: {$elemMatch: {...}}}` reads unambiguously: return name, and return one matching grade. Mixing an array operator into an exclusion projection is confusing at best; keep the plain fields setting the mode. ## Sorting inside the array Neither operator sorts. `$slice: -5` gives you the last five elements *as stored*, which is only "the five newest" if writes append in time order — for example via `$push`. If order matters, either maintain the order on write (an appending `$push`, or `$push` with `$sort` and `$slice` modifiers to keep a capped, sorted array) or sort in an aggregation with `$sortArray`. Assuming the stored order matches business order is a quiet source of wrong answers. ## What to say in an interview Name the three `$slice` forms, state that `$elemMatch` projection is condition-based and returns only the first match (and omits the field on no match), contrast it with the positional `$` which reuses the query condition, and finish with `$filter` as the way to get every matching element.
- A document has five array elements matching the $elemMatch projection condition. How many come back?One. The `$elemMatch` projection returns a one-element array holding the first matching element in stored order, and it omits the field entirely if nothing matches. To return every matching element you need the aggregation framework's `$filter` expression inside `$project`, which evaluates the condition over the whole array and keeps all hits.
- What does the positional $ projection require that $elemMatch does not?The positional `$` reuses the query's own condition on that array, so the array field must appear in the filter and you cannot give the projection a different condition. `$elemMatch` carries its own independent condition, so you can select documents on one criterion and return an element chosen by another. Both return at most one element.
- Does {comments: {$slice: -5}} reliably return the five newest comments?Only if writes append in time order — for example a plain `$push` — because `$slice` takes elements by stored position and never sorts. If elements can be inserted out of order or reordered, maintain order on write (a `$push` with the `$sort` and `$slice` modifiers keeps a capped, sorted array) or sort explicitly in an aggregation.
saying these in an interview costs you the question
- Believes $elemMatch projection returns all matching elements
- Thinks these operators filter which documents match
- Expects $slice to sort the array before slicing
- Uses positional $ without the array field in the filter
- Assumes a missing match yields an empty array, not an absent field