2.4 Projections, Cursor Methods & Collation
Key Takeaways
- Inclusion projections ('{ field: 1 }') return only the named fields plus '_id' and exclusion projections ('{ field: 0 }') return everything else; mixing the two forms in one projection document is illegal except for explicitly suppressing the primary key with '_id: 0' inside an inclusion projection.
- Array projections allow selective retrieval of array subsets via '$slice' (limiting count or pagination range), the positional projection operator '$' (returning the first matching query element), and '$elemMatch' projection.
- The 'find()' method returns a lazy cursor; chained cursor modifier methods ('sort()', 'skip()', 'limit()') are evaluated by the database engine in a deterministic internal order: 'sort' is always executed first, followed by 'skip', and finally 'limit', regardless of the syntactic chaining order in client code.
- Collation defines language-specific rules for string comparison and sorting, where 'strength: 1' performs primary base-character matching (ignoring case and accents), 'strength: 2' ignores case while respecting accents, and 'strength: 3' enforces strict case and accent sensitivity.
- countDocuments() accepts a query filter and returns an exact count, while estimatedDocumentCount() reads collection metadata for speed and accepts no filter at all; collection and cursor count() are deprecated.
Projections, Cursor Methods & Collation
When developing high-performance applications with MongoDB, optimizing data transmission across the network and controlling how documents are sorted, paginated, and compared is as critical as constructing precise query filters. This section covers Projections (restricting returned fields), Cursor Handling and Methods (sorting, pagination, and deterministic execution order), and Collation (language-aware and case-insensitive string evaluation).
Projections: Shaping Query Results
By default, a find() query returns all fields of matching documents. A projection is an optional second argument passed to find(filter, projection) that specifies which fields MongoDB should return to the client, reducing network bandwidth and client-side memory consumption.
db.collection.find(
{ <query filter> },
{ <projection specification> }
)
1. Inclusion Projections (1 or true)
In an inclusion projection, you explicitly list the fields you want returned by assigning them 1 (or true). Only the specified fields plus the _id field are included in the result.
// Returns only '_id', 'title', and 'price'
db.books.find(
{ in_stock: true },
{ title: 1, price: 1 }
);
2. Exclusion Projections (0 or false)
In an exclusion projection, you explicitly list fields to omit by assigning them 0 (or false). MongoDB returns all fields in the document except the excluded ones.
// Returns all fields EXCEPT 'internal_audit_log' and 'password_hash'
db.users.find(
{ is_active: true },
{ password_hash: 0, internal_audit_log: 0 }
);
3. The Cardinal Rule of Projections: No Mixing
You cannot mix inclusion and exclusion in a single projection document. Attempting to do so triggers a runtime exception.
// ILLEGAL PROJECTION: Throws MongoServerError: Cannot do inclusion on field price in exclusion projection
db.books.find({}, { title: 1, price: 0 });
The Single Exception: _id: 0
The only exception to the no-mixing rule is the _id field. Because _id is automatically included in inclusion projections by default, you can explicitly suppress _id using _id: 0 alongside inclusion fields:
// VALID PROJECTION: Returns ONLY 'title' and 'price' (suppresses '_id')
db.books.find({}, { title: 1, price: 1, _id: 0 });
4. Projecting Embedded Subdocument Fields
Dot notation allows projecting specific fields inside nested subdocuments:
// Project only the city from the nested address object, omitting _id
db.customers.find({}, { name: 1, "address.city": 1, _id: 0 });
Array Projection Operators: $slice, $, and $elemMatch
MongoDB provides specialized projection operators to return specific elements from array fields.
1. The $slice Projection Operator
The $slice operator controls the number of array elements returned:
// Return only the first 3 comments in the array
db.posts.find({}, { title: 1, comments: { $slice: 3 } });
// Return the last 2 comments in the array
db.posts.find({}, { title: 1, comments: { $slice: -2 } });
// Pagination: Skip 10 comments and return up to 5 comments ([skip, limit])
db.posts.find({}, { title: 1, comments: { $slice: [ 10, 5 ] } });
2. The Positional Projection Operator ($)
The positional $ operator projects the first array element that matched the query condition specified in the query filter document.
// Find students with a score >= 90 and return ONLY the first matching score element
db.students.find(
{ "grades.score": { $gte: 90 } },
{ name: 1, "grades.$": 1 }
);
Constraint: The query filter must contain a condition on the array field being projected with
$.
3. The $elemMatch Projection Operator
The $elemMatch projection operator filters array elements during projection based on an independent condition, returning the first element that satisfies the criteria:
// Returns the first zip code entry where state is 'NY', regardless of query filter
db.schools.find(
{ active: true },
{ name: 1, zip_codes: { $elemMatch: { state: "NY" } } }
);
Cursors and Cursor Methods
The db.collection.find() method does not immediately return all matching documents over the network. Instead, it returns a cursor—an iterable pointer to the result set on the server.
Client Application MongoDB Server (mongod)
| |
| --- find(query) --------------------------> | (Opens Cursor)
| <--- First Batch (101 docs or 1 MB) ------- | (Streams initial batch)
| |
| --- getMore (Iterate next batch) ---------> | (Fetches next 16 MiB batch)
| <--- Next Batch --------------------------- |
| |
| (Cursor exhausted or closed) | (Releases cursor memory)
Core Cursor Modifier Methods
sort(): Orders the result set.1indicates ascending order (A to Z, 0 to 9);-1indicates descending order (Z to A, 9 to 0).skip(): Skips the specified number of documents from the beginning of the result set.limit(): Restricts the maximum number of documents returned by the cursor.
// Sort products by price descending, skip the first 20 (page 2), and limit to 10
const cursor = db.products.find({ in_stock: true })
.sort({ price: -1 })
.skip(20)
.limit(10);
Deterministic Internal Execution Order: sort $\rightarrow$ skip $\rightarrow$ limit
A classic topic on the MongoDB certification exam is understanding how MongoDB executes chained cursor methods. Regardless of the syntactic order in which methods are chained in client application code, the MongoDB query engine always applies operations in this strict deterministic sequence:
// Code Pattern A:
db.users.find().sort({ score: -1 }).skip(5).limit(10);
// Code Pattern B (Chained in different syntactic order):
db.users.find().limit(10).skip(5).sort({ score: -1 });
Both Code Pattern A and Code Pattern B produce the EXACT same result set. The engine sorts the entire matched set by score: -1, skips the top 5 highest-scoring documents, and returns the next 10 documents.
Exam Trap: Calling
.limit(10).skip(5)does NOT limit the collection to 10 documents and then skip 5 of those 10. The engine ALWAYS sorts first, skips next, and applies the limit last.
Cursor Timeout Mechanics
By default, MongoDB automatically closes server-side cursors after 10 minutes of inactivity (the cursorTimeoutMillis server parameter, default 600000). In long-running batch processing jobs, developers can prevent timeout via .noCursorTimeout(), but the cursor must be explicitly closed manually to prevent memory leaks on the server.
Collation: Language-Specific and Case-Insensitive Matching
Collation specifies language-specific rules for string comparison, such as rules for lettercase, accent marks, and character ordering.
The Collation Document Structure
{
locale: <string>, // e.g., "en", "fr", "es", "simple"
strength: <integer>, // 1, 2, 3, 4, or 5
caseLevel: <boolean>,
caseFirst: <string>,
numericOrdering: <boolean>
}
Collation Strength Levels
The strength parameter controls the level of comparison strictness:
| Strength Level | Name | What it Distinguishes | Equality Example |
|---|---|---|---|
1 | Primary | Base characters only (Ignores case and accents) | "role" == "Role" == "rôle" |
2 | Secondary | Base characters + Accents (Ignores case, respects accents) | "role" == "Role" != "rôle" |
3 (Default) | Tertiary | Base characters + Accents + Case | "role" != "Role" != "rôle" |
Applying Collation for Case-Insensitive Lookups
To perform a case-insensitive query or sort in English without expensive regexes, use strength: 2 (or strength: 1):
// Case-insensitive find: Matches "admin", "Admin", "ADMIN", "aDmIn"
db.users.find({ username: "admin" })
.collation({ locale: "en", strength: 2 });
Collation Hierarchy
Collation can be defined at three levels:
- Collection Level: Defined during
db.createCollection("users", { collation: { locale: "en", strength: 2 } })as the default for all operations. - Index Level: Applied to specific indexes (
db.users.createIndex({ username: 1 }, { collation: { locale: "en", strength: 2 } })). - Operation Level: Specified dynamically on individual
.find(),.sort(), or.aggregate()calls.
Counting Matching Documents
Exam objective 2.17 asks you to identify the expression used to count matching documents, and MongoDB offers three that behave very differently. Picking the wrong one is a correctness bug, not a style choice.
countDocuments(filter) — accurate
db.collection.countDocuments() accepts a query filter and returns an exact count. It is not a metadata lookup: MongoDB documents that it wraps the following aggregation and returns the value of n:
db.orders.countDocuments({ status: "SHIPPED" })
// equivalent to:
db.orders.aggregate([
{ $match: { status: "SHIPPED" } },
{ $group: { _id: null, n: { $sum: 1 } } }
])
Because it actually resolves documents, the count stays correct after an unclean shutdown and correctly excludes orphaned documents on a sharded cluster. The cost is speed — counting every document in a very large collection is proportional to the number of matches. countDocuments({}) on a huge collection is the slow path, and an index covering the filter is what keeps it fast.
estimatedDocumentCount() — fast
db.collection.estimatedDocumentCount() reads the collection metadata instead of scanning, which makes it dramatically faster — but it comes with a hard restriction that is the single most testable fact here:
estimatedDocumentCount()does not accept a query filter. It only ever counts the whole collection.
Because it trusts metadata, its result can be wrong after an unclean shutdown or a file-copy-based initial sync, and it does not filter out orphaned documents on a sharded cluster.
count() — deprecated
db.collection.count() and cursor.count() are deprecated; the drivers deprecated their count() APIs in favor of the two methods above. Without a query predicate, count() falls back to collection metadata and inherits exactly the same inaccuracy caveats. It also cannot be used inside a transaction. If an answer option uses count(), it is almost always the distractor.
Choosing Correctly
| Requirement | Correct expression |
|---|---|
| Exact count of documents matching a filter | countDocuments(filter) |
| Exact count of an entire collection | countDocuments({}) |
| Fast approximate size of an entire collection | estimatedDocumentCount() |
| Count inside an aggregation pipeline | { $count: "fieldName" } |
| Count inside a transaction | countDocuments() (never count()) |
Exam trap: an item that asks for "the number of documents where
statusisACTIVE" and offersdb.orders.estimatedDocumentCount({ status: "ACTIVE" })is testing the filter restriction. That call does not filter — the argument is not a query predicate — so the correct answer iscountDocuments({ status: "ACTIVE" }).
Which of the following projection documents will cause MongoDB to throw a runtime syntax exception due to invalid projection mixing rules?
A developer writes the query 'db.products.find().limit(5).skip(15).sort({ price: 1 })'. In what order does the MongoDB query engine evaluate these operations?
An application executes 'db.users.find({ "logins.ip": "192.168.1.1" }, { username: 1, "logins.$": 1 })'. What data is returned in the 'logins' array of matching documents?
Which collation configuration allows a query to perform case-insensitive string comparisons while still distinguishing between characters with different accent marks (e.g., matching 'resume' and 'Resume', but not 'résumé')?
A reporting endpoint must return the exact number of orders whose status field equals 'ACTIVE'. Which expression is correct?