Projection: Selecting Fields
When you query MongoDB, find() returns whole documents by default — every field, no matter how large. Projection lets you tell MongoDB which fields to include or exclude in the results, so you only get back the data you actually need. This matters for performance: less data crossing the network, less memory used to build result documents, and (in some cases) queries that can be satisfied entirely from an index without ever touching the underlying document.
Overview / How it works
A MongoDB document is BSON, not plain JSON — it can hold types like ObjectId, Date, and binary data, and documents in the same collection are not required to share a schema. That flexibility means documents can grow to hold many fields, some of them large (embedded arrays, long text, binary blobs). If your application only needs a user’s name and email, pulling back the full document — including a large preferences object or a passwordHash you should never expose — wastes bandwidth and risks leaking sensitive fields to client code.
Projection is the second argument to find() (and to findOne()), or the value of a $project stage in an aggregation pipeline. It is a document whose keys are field names and whose values are 1 (include) or 0 (exclude). MongoDB applies the projection after the query filter has selected matching documents — it does not change which documents match, only which fields of those documents are returned.
There are two projection modes, and as a rule you cannot mix them in the same projection document:
- Inclusion projection — list the fields you want (
{ name: 1, email: 1 }). Every field you don’t list is dropped, except_id, which is included automatically unless you explicitly set_id: 0. - Exclusion projection — list the fields you want removed (
{ passwordHash: 0 }). Every other field is returned.
The one place these two modes can be combined is _id: you can write { name: 1, email: 1, _id: 0 } (inclusion projection that also drops _id), because _id is a special case exempt from the mixing rule.
Projection also works on nested (embedded) fields using dot notation, e.g. { "address.city": 1 }, and on arrays via operators like $slice, $elemMatch, and the positional $ operator, which let you return only part of an array instead of the whole thing.
Syntax
db.collection.find(
<query filter>,
<projection>
);
- <query filter> — the usual filter document that selects which documents match, e.g.
{ status: "active" }. - <projection> — a document of
fieldName: 1orfieldName: 0pairs (inclusion or exclusion, not mixed, except for_id). Can also use array operators ($slice,$elemMatch) or, in an aggregation pipeline, computed expressions.
In an aggregation pipeline, the equivalent is a $project stage:
db.collection.aggregate([
{ $match: <query filter> },
{ $project: <projection> }
]);
Examples
Example 1: Inclusion projection
Suppose db.users holds documents with name, email, age, passwordHash, and an embedded address. To return only the name and email for active users:
db.users.find(
{ status: "active" },
{ name: 1, email: 1 }
);
Output:
[
{ _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d1"), name: "Priya Shah", email: "priya@example.com" },
{ _id: ObjectId("64f1a2b3c4d5e6f7a8b9c0d2"), name: "Tom Reyes", email: "tom@example.com" }
]
Only name, email, and the automatically-included _id come back. age, passwordHash, and address are gone entirely — not null, simply absent from the returned documents.
Example 2: Exclusion projection and dropping _id
Sometimes it’s easier to say what you don’t want. To return everything except the sensitive passwordHash field, and also drop _id because the client doesn’t need it:
db.users.find(
{ status: "active" },
{ passwordHash: 0, _id: 0 }
);
Output:
[
{ name: "Priya Shah", email: "priya@example.com", age: 29, address: { city: "Mumbai", zip: "400001" } },
{ name: "Tom Reyes", email: "tom@example.com", age: 34, address: { city: "Austin", zip: "73301" } }
]
Every field except passwordHash and _id is returned, including the nested address object in full. This is the classic pattern for "return the whole user record, minus the secret."
Example 3: Nested fields and array slicing
Projection understands dot notation for embedded fields, and the $slice operator for trimming arrays. Given a db.orders collection where each order has an items array and a shipping sub-document:
db.orders.find(
{ customerId: "cust_1001" },
{ "shipping.city": 1, items: { $slice: 2 } }
);
Output:
[
{
_id: ObjectId("64f1a2b3c4d5e6f7a8b9c0e5"),
shipping: { city: "Chicago" },
items: [
{ sku: "SKU-100", qty: 2 },
{ sku: "SKU-204", qty: 1 }
]
}
]
Only shipping.city is pulled from the shipping sub-document (other shipping fields like zip or carrier are dropped), and $slice: 2 returns just the first two elements of the items array even if the order has ten line items. $slice also accepts a negative number (last N elements) or a two-element array [skip, limit] for pagination within the array itself.
How it works step by step
- MongoDB’s query planner first evaluates the filter document to find matching documents, using an index if one exists and is selected by the planner (check with
explain()to confirmIXSCANvs a fullCOLLSCAN). - For each matching document, the storage engine normally has to load the full BSON document from disk/cache before applying anything else.
- The projection document is then applied in memory, stripping out any field not requested (inclusion mode) or removing exactly the listed fields (exclusion mode), before the result is serialized and sent back over the wire.
- Covered queries are the exception: if every field in both the filter and the projection is part of a single index (and
_idis explicitly excluded, unless the index covers it too), MongoDB can answer the query directly from the index entries without ever loading the full document — the fastest possible read.
Common Mistakes
Mistake 1: Mixing inclusion and exclusion
MongoDB rejects a projection that mixes 1s and 0s on regular fields (only _id is exempt):
// Wrong: mixes inclusion (name: 1) with exclusion (age: 0)
db.users.find({}, { name: 1, age: 0 });
// Error: Cannot do inclusion on field name in exclusion projection
Pick one mode. If you want everything except age, use pure exclusion; if you want only name, use pure inclusion:
db.users.find({}, { age: 0 });
// or
db.users.find({}, { name: 1 });
Mistake 2: Assuming excluded fields are null
A field left out of an inclusion projection is not present in the returned document at all — it is not set to null. Code that checks if (user.age === null) after a projection that omitted age will get undefined, not null, which can silently break strict equality checks. Always check with 'age' in user or user.age === undefined when the field may have been projected out.
Mistake 3: Comparing a projected-out _id to a string
Even when a projection is applied, a returned _id is still an ObjectId, not a string. Comparing it directly to a string id pulled from a URL parameter always fails:
// Wrong: req.params.id is a string, doc._id is an ObjectId
if (doc._id === req.params.id) { /* never true */ }
Convert first:
import { ObjectId } from "mongodb";
if (doc._id.equals(new ObjectId(req.params.id))) { /* correct */ }
Best Practices
- Always project out sensitive fields (password hashes, internal tokens, API keys) explicitly rather than relying on the application layer to filter them after the fact.
- Prefer inclusion projections for API responses so that a newly-added, unreviewed field on the document can never leak to clients by accident.
- When you need only a handful of fields from a large collection, consider a supporting index that covers both the filter and the projected fields to get a covered query.
- Use
$sliceinstead of pulling a whole array when you only need a preview (e.g. the first 3 comments on a post). - In aggregation pipelines, put
$projectafter$matchso you filter documents down first and only reshape the ones you actually kept. - Remember dot notation for nested fields (
"address.city": 1) instead of trying to project a sub-field with a nested object literal, which is not valid projection syntax.
Practice Exercises
- Given a
db.productscollection with fieldsname,price,description, andsupplier(an embedded object withcontactEmailandaddress), write a query that returns onlyname,price, andsupplier.contactEmailfor products wherepriceis greater than 50. - Write a query on
db.ordersthat returns every field exceptinternalNotesand_id, for orders withstatus: "delivered". - Given a
db.postscollection where each post has acommentsarray, write a query that returns a post’stitleplus only the last 5 comments in the array. (Hint:$sliceaccepts a negative number.)
Summary
- Projection is the second argument to
find()/findOne(), using1to include and0to exclude fields. - You cannot mix inclusion and exclusion in one projection, except that
_idcan always be turned off alongside an inclusion projection. _idis included by default unless you explicitly set_id: 0.- Fields left out of an inclusion projection are absent from the result, not
null. - Dot notation projects nested fields;
$slicetrims arrays to a subset instead of returning them in full. - A projection that matches an index exactly can produce a covered query, avoiding a document fetch entirely.
- In aggregation pipelines, the same idea is expressed with a
$projectstage, ideally placed after$match.
