Projections in MongoDB in MongoDB, NoSQL & Data Modeling
When you query a MongoDB collection, you often get back more data than you actually need. A user profile query might return gigabytes of embedded media, a product document might include massive content bodies, and an order might carry denormalized histories. Projections let you specify exactly which fields to return from a query, cutting down network overhead, reducing memory consumption, and improving application performance. Today we'll explore how to use projections effectively and when they unlock query optimization opportunities.
Pass a Projection as the Second Argument
In MongoDB, the projection is the second argument to query methods like find(), findOne(), and aggregate(). It's a document where you explicitly include or exclude fields from the result set.
The basic syntax uses field names with inclusion (1) or exclusion (0) flags:
// Include only name and email
db.users.findOne(
{ _id: ObjectId("...") },
{ name: 1, email: 1 }
);
// Exclude password and sensitive data
db.users.find(
{},
{ password: 0, apiKey: 0, internalNotes: 0 }
);
There's one special rule: you cannot mix inclusions and exclusions in the same projection, except for the _id field. The _id field is always included by default, but you can explicitly exclude it if needed.
Here's a more practical example from a real e-commerce scenario. Imagine we have a products collection with detailed specifications, raw supplier data, and large image metadata:
db.products.find(
{ category: "electronics" },
{
_id: 1,
name: 1,
price: 1,
inStock: 1,
supplierNotes: 0,
rawImageData: 0,
manufacturerSpecs: 0
}
);
In a Node.js driver context using the MongoDB client, this looks like:
const result = await collection.find(
{ category: "electronics" },
{ projection: { name: 1, price: 1, inStock: 1 } }
).toArray();
Exclude Large Fields Like Content Bodies
One of the most impactful uses of projections is removing fields that consume significant storage or transfer overhead. Content bodies, raw HTML, image data, large JSON blobs, and audit logs can easily dominate a document's size.
Consider a blog or CMS collection. Posts might look like this in their raw form:
{
_id: ObjectId("..."),
title: "MongoDB Performance Tips",
slug: "mongodb-performance-tips",
author: "Engineering Team",
publishedAt: ISODate("2024-01-15"),
body: "... 50KB of HTML content ...",
rawMarkdown: "... 30KB of markdown ...",
editHistory: [/* 100+ revisions */],
internalTags: ["draft-v3", "reviewed", "pending-approval"],
viewCount: 1523
}
When fetching a list of blog posts for the homepage feed, you'd want the title, slug, author, and metadata—but never the full body or edit history:
db.posts.find(
{ publishedAt: { $lt: new Date() } },
{
title: 1,
slug: 1,
author: 1,
publishedAt: 1,
viewCount: 1,
body: 0,
rawMarkdown: 0,
editHistory: 0,
internalTags: 0
}
).sort({ publishedAt: -1 }).limit(20);
This projection ensures each document returned is perhaps 1-2KB instead of 80KB+. When you're paginating through hundreds of posts, that difference compounds quickly.
In a real SaaS or trading platform, consider order documents that track full interaction history. You might have a listing view that only needs summary data:
db.orders.find(
{ userId: "user123", status: { $ne: "cancelled" } },
{
_id: 1,
orderId: 1,
createdAt: 1,
totalAmount: 1,
status: 1,
itemCount: 1,
// Exclude these large fields
eventLog: 0,
paymentTransactionHistory: 0,
carrierTrackingData: 0,
customerCommunications: 0
}
).sort({ createdAt: -1 });
The rule of thumb: if you're not using a field in the immediate response or rendering context, exclude it. This is especially critical when returning lists or paginated results.
Reduces Network and Memory
Projections deliver tangible performance gains in two layers: network I/O and application memory.
Network I/O: MongoDB sends data from the server to your application over a network connection. Reducing the document size by 90% means 90% less data traversing that connection. In distributed systems—especially cloud environments where inter-region or inter-availability-zone traffic incurs latency and sometimes cost—this matters significantly.
Memory: Your application must deserialize and hold returned documents in memory. If you fetch 1,000 documents at 80KB each, that's 80MB in memory. With projections narrowing each to 2KB, you're now using 2MB. For high-traffic endpoints or batch operations, this difference determines whether you can process the data in one pass or need to paginate.
Consider a real-world scenario: a ride-hailing platform querying active drivers in a region to display on a map. Each driver record might contain:
{
_id: ObjectId("..."),
driverId: "driver_456",
name: "Alice",
lat: 40.7128,
lng: -74.0060,
status: "active",
rating: 4.8,
phoneVerified: true,
// Large fields not needed for map view
fullDocumentationHistory: "... 50KB ...",
bankDetails: { /* sensitive */ },
previousTripsLog: [ /* 1000s of entries */ ],
serviceAgreementPDF: "... binary data ..."
}
When rendering the live map, you'd use:
db.drivers.find(
{ status: "active", city: "NYC" },
{ driverId: 1, name: 1, lat: 1, lng: 1, rating: 1 }
);
Now each document is maybe 200 bytes instead of 50KB. With 500 active drivers on screen, that's 100KB total instead of 25MB. The map render becomes snappy; the server can handle more concurrent requests with the same hardware.
Covered Queries When the Index Has All Fields
This is where projections unlock a powerful optimization: covered queries. When an index contains all the fields needed to satisfy a query and its projection, MongoDB can answer the query entirely from the index without touching the collection's main data storage.
Here's how it works: normally, MongoDB finds matching documents using an index (which is much smaller and faster to scan), then fetches the full documents from the main collection. With a covered query, MongoDB never needs that second step because the index itself has everything needed.
To create a covered query, you need an index on all fields used in the query predicate and the projection. For example:
// Create a compound index
db.users.createIndex({ status: 1, email: 1, name: 1 });
// This query can be covered
db.users.find(
{ status: "active" },
{ email: 1, name: 1, _id: 0 }
);
MongoDB's explain output will show "stage": "COLLSCAN" becoming "stage": "IXSCAN" when you use projections that enable covering. For high-traffic queries, covered queries can be 10-50x faster because they avoid disk seeks entirely.
Let's build a practical example from a CRM. You frequently need to fetch active contacts by region for email campaigns:
// Index that can cover the query
db.contacts.createIndex({
region: 1,
status: 1,
email: 1,
firstName: 1,
lastName: 1
});
// Covered query—no collection access needed
db.contacts.find(
{ region: "US-West", status: "active" },
{ email: 1, firstName: 1, lastName: 1, _id: 0 }
);
The order of fields in the index matters. Query predicates should come first, then sort fields (if any), then projection fields. This structure lets MongoDB scan the index efficiently without jumping around.
In the Node.js driver with explain:
const explain = await collection.find(
{ region: "US-West", status: "active" },
{ projection: { email: 1, firstName: 1, lastName: 1, _id: 0 } }
).explain("executionStats");
console.log(explain.executionStats.stage); // "IXSCAN" for covered query
console.log(explain.executionStats.docsExamined); // should equal docsReturned
When docsExamined equals docsReturned, you know the query is covered—no unnecessary document fetches.
Common Mistakes
Mixing inclusions and exclusions: You'll get an error if you try { name: 1, password: 0 } unless you're excluding _id. Decide on a strategy: include what you need, or exclude what you don't.
Forgetting about _id: The _id field is always included by default. If you don't want it in your projection, explicitly set _id: 0. This is especially relevant for covered queries where every byte counts.
Projecting fields that don't exist: If a field doesn't exist in a document, the projection simply won't include it—no error is thrown. This is usually fine, but it can mask data inconsistency issues during development.
Over-projecting in aggregation pipelines: In $project stages within aggregation pipelines, you can do computed fields and transformations. Don't use a simple find() projection where you need complex field logic—use aggregation instead.
Not measuring the impact: Always use explain() to verify that your projections are actually improving query execution. A projection might reduce network overhead but not the query's core computational cost if the query plan remains the same.
Key Takeaways
- Pass a projection document as the second argument to
find(),findOne(), and other query methods to control which fields are returned. - Exclude large, rarely-used fields like content bodies, raw data, and audit histories to cut network transfer and memory usage significantly.
- Use projections strategically to reduce document sizes in list and paginated views, where the overhead compounds across many records.
- Combine projections with indexes to enable covered queries, where MongoDB serves results entirely from the index without touching the main collection.
- Always verify query plans with
explain()to confirm that projections are delivering the expected performance improvements. - Remember that
_idis included by default; explicitly exclude it if you don't need it, especially in covered query scenarios.






Comments
Loading comments…