← back

Projection

FILE  14_projection
TOPIC  Include · Exclude · _id · $slice · $elemMatch · Covered Queries
LEVEL  Foundation
01
Basics
What projection does and how it works
core

Projection is the second argument to find() that controls which fields are returned in each result document. Rather than transferring every stored field, you shape the output to only what your application actually needs.

db.collection.find(/* filter */, /* projection */)

Two Mutually Exclusive Modes

ModeSyntaxBehavior
Inclusion { field: 1 } Return ONLY the listed fields (plus _id by default)
Exclusion { field: 0 } Return ALL fields EXCEPT the listed ones
RULE
You cannot mix inclusion (1) and exclusion (0) in the same projection document. The only exception is _id, which can be set to 0 alongside inclusion fields.

The _id Special Case

_id is always included in results unless you explicitly set { _id: 0 }. This applies even in inclusion mode — MongoDB always returns _id unless you opt out.

Why Use Projection?

  • Reduces network payload — only transmit the bytes your application needs
  • Reduces memory usage — smaller working documents in your app and driver
  • Enables covered queries — satisfy queries entirely via index with no document fetch
  • Protects sensitive data — exclude fields like password or ssn at the query layer
// No projection — full document returned for every match
db.users.find({ active: true })

// With projection — only name and email returned
db.users.find({ active: true }, { name: 1, email: 1 })
02
Inclusion Mode
Specify exactly which fields to return
include

In inclusion mode you list every field you want to receive. Only those fields (plus _id) appear in the result. Any field not listed is omitted regardless of whether it exists in the document.

Syntax

{ field: 1, anotherField: 1 }

Example 1 — Return Only name and email

db.users.find(
  { active: true },
  { name: 1, email: 1 }
)
// Result documents look like:
// { _id: ObjectId("64a1f2..."), name: "Alice", email: "alice@example.com" }
// _id is automatically included — you did not request it but it appears anyway

Example 2 — Return name, email, and Suppress _id

db.users.find(
  { active: true },
  { name: 1, email: 1, _id: 0 }
)
// Result documents:
// { name: "Alice", email: "alice@example.com" }
// Combining _id: 0 with inclusions is the ONE legal mixing exception

Example 3 — Nested Field with Dot Notation

db.users.find(
  {},
  { "address.city": 1, name: 1, _id: 0 }
)
// Source document:
// { name: "Alice", address: { city: "Berlin", zip: "10115", street: "Unter den Linden 6" } }
//
// Result:
// { name: "Alice", address: { city: "Berlin" } }
//
// The "address" key is preserved but only "city" survives inside it.
// "zip" and "street" are stripped. The parent wrapper key is always kept.
NOTE
When projecting a nested sub-field like "address.city": 1, MongoDB preserves the parent key address but strips all sibling sub-fields. Only the projected path survives inside the parent object.

Quick Reference

ProjectionFields Returned
{ name: 1 }_id + name
{ name: 1, email: 1 }_id + name + email
{ name: 1, _id: 0 }name only
{ "address.city": 1, _id: 0 }address.city only (parent key preserved)
03
Exclusion Mode
Return everything except specified fields
exclude

In exclusion mode MongoDB returns every field in the document except the ones you set to 0. This is the right choice when documents are wide and you only need to hide a small number of sensitive or unnecessary fields.

Syntax

{ sensitiveField: 0, anotherField: 0 }

Example 1 — Exclude password from All User Queries

db.users.find(
  { active: true },
  { password: 0 }
)
// All fields returned EXCEPT password:
// { _id: ObjectId("..."), name: "Alice", email: "alice@example.com",
//   createdAt: ISODate("..."), role: "admin", lastLogin: ISODate("...") }
// password is silently absent

Example 2 — Exclude Multiple Sensitive Fields

db.users.find(
  {},
  { password: 0, ssn: 0, tokenHash: 0, refreshToken: 0 }
)
// All four sensitive fields stripped from every document in the result.
// Every other field including _id passes through untouched.
ERROR
Mixing modes throws a server error immediately:

db.col.find({}, { name: 1, age: 0 })
MongoServerError: Projection cannot have a mix of inclusion and exclusion.

The one legal exception: { name: 1, _id: 0 } — you may exclude _id alongside inclusion fields.

Common Use Cases for Exclusion Mode

  • Sensitive credentialspassword, ssn, apiKey, tokenHash
  • Internal system fields__v (Mongoose version key), __proto
  • Heavy embedded arrays — strip large arrays when you only need the parent metadata
  • Binary / blob data — exclude raw bytes, thumbnails, or base64 blobs from listing endpoints
WARN
Exclusion mode only hides known fields. If a new field is added to your schema later (e.g., twoFactorSecret), it will appear in all results until you explicitly exclude it. For dynamic schemas, prefer inclusion mode to control the output shape precisely.

Validity Summary

ProjectionEffectValid?
{ password: 0 }Exclude one field, return all othersyes
{ a: 0, b: 0, c: 0 }Exclude multiple fieldsyes
{ name: 1, age: 0 }Mix include + excludeerror
{ name: 1, _id: 0 }Include name, suppress _idyes
04
Array Projection
$slice and $elemMatch operators
arrays

MongoDB provides two dedicated projection operators for arrays. $slice limits how many elements are returned from an array. $elemMatch returns only the first element that satisfies a condition.

$slice — Control How Many Array Elements Are Returned

$slice in a projection does not filter by value — it simply slices the array by position, like JavaScript's Array.prototype.slice.

// First 3 comments from each post
db.posts.find({}, { title: 1, comments: { $slice: 3 } })

// Last 2 comments (negative index = from the end)
db.posts.find({}, { title: 1, comments: { $slice: -2 } })

// Skip 5, take the next 3 — useful for array-level pagination preview
db.posts.find({}, { title: 1, comments: { $slice: [5, 3] } })

// Zero elements — return the array key but with an empty array
db.posts.find({}, { title: 1, comments: { $slice: 0 } })

$slice Reference

ExpressionMeaning
{ $slice: N } (positive)First N elements
{ $slice: -N } (negative)Last N elements
{ $slice: [skip, N] }Skip skip elements, then take N
{ $slice: 0 }Empty array (zero elements returned)
TIP
Use $slice: 1 or $slice: 3 to generate preview excerpts in list views — show the first comment, first three tags, or latest N activity items without loading entire embedded arrays.

$elemMatch in Projection — Return First Matching Element

Unlike $elemMatch used in a query filter (which selects documents), $elemMatch in a projection returns only the first array element that matches the condition from each result document. Non-matching elements are suppressed.

// Return only the first grade with score >= 90
db.students.find(
  { "grades.score": { $gte: 90 } },            // query: document must have at least one
  { name: 1, grades: { $elemMatch: { score: { $gte: 90 } } } }   // projection: first match only
)

// Source document:
// { name: "Bob", grades: [{ score: 95, subject: "Math" }, { score: 92, subject: "Physics" }, { score: 78, subject: "History" }] }
//
// Result:
// { _id: ObjectId("..."), name: "Bob", grades: [{ score: 95, subject: "Math" }] }
//
// ONLY the first element matching score >= 90 is returned — not all matches.
// { score: 92, subject: "Physics" } is suppressed even though it also qualifies.
// Real-world: show first active subscription for each user
db.users.find(
  { "subscriptions.status": "active" },
  { name: 1, subscriptions: { $elemMatch: { status: "active" } }, _id: 0 }
)
// Even if a user has 3 active subscriptions, the projection returns only the first one.
CRITICAL
$elemMatch in projection always returns at most one element — the first match. It is NOT a "filter all matching elements" operator. If you need all matching elements, use the aggregation pipeline with $filter.

$slice vs $elemMatch Comparison

OperatorFilters by value?Limits count?How many returned?
$sliceNo — positional onlyYesFirst/last N (any value)
$elemMatch (projection)Yes — by conditionImplicit (max 1)First matching element only
05
Covered Queries
Queries satisfied entirely by an index
performance

A covered query is one where MongoDB satisfies the entire operation — filter, sort, and projection — using only an index, without ever fetching the actual document from the collection storage. This eliminates the FETCH stage entirely and is the fastest possible query path.

Requirements for a Covered Query

  • Every query filter field must be part of the same index
  • Every projected field must be part of the same index
  • _id must be explicitly excluded (_id: 0) unless _id itself is in the index
  • The query must not touch embedded documents outside index paths

Step-by-Step Example

// Step 1: Create a compound index covering both query and projection fields
db.users.createIndex({ email: 1, name: 1 })

// Step 2: Query on "email" and project only "email" and "name", suppress _id
db.users.find(
  { email: "alice@example.com" },
  { email: 1, name: 1, _id: 0 }
)
// MongoDB reads only the B-tree index entries — the collection data files are never touched.
// Result: { email: "alice@example.com", name: "Alice" }

Verifying Coverage with explain()

db.users.find(
  { email: "alice@example.com" },
  { email: 1, name: 1, _id: 0 }
).explain("executionStats")

// COVERED — good output:
// winningPlan.stage: "IXSCAN"
// No "FETCH" stage anywhere in the plan tree
// totalDocsExamined: 0    ← documents never touched
// totalKeysExamined: 1    ← only index keys scanned

// NOT COVERED — bad output:
// winningPlan: { stage: "FETCH", inputStage: { stage: "IXSCAN" } }
// totalDocsExamined: 1    ← documents were loaded from collection storage
TIP
For read-heavy workloads — leaderboards, autocomplete, analytics aggregations — designing covered queries can reduce query latency by an order of magnitude compared to full document fetches, especially when documents are large.

Why _id Must Be Excluded

// Index: { email: 1, name: 1 }
// _id is NOT part of this index

// This query is NOT covered — _id is implicitly included in projection:
db.users.find(
  { email: "alice@example.com" },
  { email: 1, name: 1 }       // _id appears in result by default → forces document fetch
)

// Fix: explicitly suppress _id → now fully covered:
db.users.find(
  { email: "alice@example.com" },
  { email: 1, name: 1, _id: 0 }
)
NOTE
If your index explicitly includes _id (e.g., { _id: 1, email: 1 }), you do not need to exclude it. Standard secondary indexes do not include _id by default.
06
Edge Cases
Gotchas and boundary behaviors
gotchas

Mixed Inclusion / Exclusion Throws Immediately

// ILLEGAL — throws on the server before any documents are scanned
db.col.find({}, { name: 1, age: 0 })
// MongoServerError: Projection cannot have a mix of inclusion and exclusion.

// The only legal exception:
db.col.find({}, { name: 1, _id: 0 })   // valid — _id: 0 may coexist with inclusions

_id Is Always Included Until Explicitly Excluded

db.col.find({}, { name: 1 })
// Result: { _id: ObjectId("64a1f2..."), name: "Alice" }   ← _id appears automatically

db.col.find({}, { name: 1, _id: 0 })
// Result: { name: "Alice" }   ← _id now gone

$slice Does Not Filter — It Limits by Position

// Misconception: "$slice: 3 returns only elements where value > something"
// Reality: it returns the first 3 elements regardless of their values

db.posts.find({}, { comments: { $slice: 3 } })
// comments: [comment0, comment1, comment2]  ← first three in stored order, no filtering

$elemMatch in Projection Returns FIRST Match Only

// Document: { grades: [{ score: 95 }, { score: 92 }, { score: 88 }] }
// Both 95 and 92 satisfy score >= 90, but only the FIRST is returned:

db.students.find(
  {},
  { grades: { $elemMatch: { score: { $gte: 90 } } } }
)
// Result: { _id: ..., grades: [{ score: 95 }] }
// { score: 92 } is dropped even though it also matched.
// Use $filter in aggregation if you need ALL matching elements.

Nested Field Projection Preserves Parent Key

// Source: { address: { city: "Berlin", zip: "10115", street: "Unter den Linden 6" } }

db.col.find({}, { "address.city": 1, _id: 0 })
// Result: { address: { city: "Berlin" } }
// "address" key is preserved as a wrapper — but only "city" survives inside it

Empty Projection and Non-Existent Fields

db.col.find({}, {})   // same as no projection — all fields returned
db.col.find({})       // identical result

// Projecting a field that does not exist in the document — no error, field simply absent:
db.col.find({}, { missingField: 1, name: 1, _id: 0 })
// Result: { name: "Alice" }   ← missingField silently absent, no error thrown
WARN
Silently absent fields can mask schema inconsistencies. If a field you expect is missing from projection results, verify it actually exists in the source document — do not assume it is a projection bug before checking the data itself.

Edge Case Summary

ScenarioBehavior
{ a: 1, b: 0 }MongoServerError — mixed modes forbidden
{ a: 1, _id: 0 }valid — _id is the only legal exception
{}All fields returned (same as no projection)
Non-existent field in projectionSilently absent in result — no error
$slice: NPositional slice, does not filter by value
$elemMatch in projectionReturns first matching element only — not all