$project
$project shapes documents on the way out: pick fields, rename, compute, exclude. The first stage you learn after $match.
MongoDB — $project
EXAMPLE
// Collection: orders
// {
// _id: ObjectId('...'),
// customer: { id: 'c-1', name: 'Alex Chen', email: 'alex@example.com' },
// total_cents: 4995,
// lines: [
// { sku: 'A-1', qty: 2, price_cents: 1995 },
// { sku: 'B-2', qty: 1, price_cents: 1005 },
// ],
// tags: ['retail', 'shipped'],
// created_at: ISODate('2024-04-10T03:14:00Z'),
// }
// ===== 1. Pick fields (include) =====
db.orders.aggregate([
{ $project: { customer: 1, total_cents: 1 } }
]);
// _id is always included unless you exclude it explicitly:
db.orders.aggregate([
{ $project: { _id: 0, customer: 1, total_cents: 1 } }
]);
// ===== 2. Exclude fields =====
db.orders.aggregate([
{ $project: { 'customer.email': 0, lines: 0 } }
]);
// You cannot mix include and exclude (other than _id) in the same $project.
// ===== 3. Rename + compute =====
db.orders.aggregate([
{ $project: {
_id: 0,
order_id: '$_id',
who: '$customer.name',
total_aud: { $divide: ['$total_cents', 100] },
line_count: { $size: '$lines' },
first_sku: { $arrayElemAt: ['$lines.sku', 0] },
}}
]);
// ===== 4. Conditional fields =====
db.orders.aggregate([
{ $project: {
who: '$customer.name',
tier: {
$switch: {
branches: [
{ case: { $gte: ['$total_cents', 10000] }, then: 'gold' },
{ case: { $gte: ['$total_cents', 3000] }, then: 'silver' },
],
default: 'bronze',
}
}
}}
]);
// ===== 5. Reshape nested arrays =====
db.orders.aggregate([
{ $project: {
_id: 0,
who: '$customer.name',
line_totals: {
$map: {
input: '$lines',
as: 'l',
in: { sku: '$$l.sku', total: { $multiply: ['$$l.qty', '$$l.price_cents'] } }
}
}
}}
]);
// ===== 6. $addFields vs $project =====
// $project replaces the document shape (only the fields you list survive).
// $addFields keeps everything and adds/overwrites.
db.orders.aggregate([
{ $addFields: { line_count: { $size: '$lines' } } }
]);
// ===== 7. $unset: opposite of include =====
db.orders.aggregate([
{ $unset: ['lines', 'customer.email'] }
]);
// ===== Patterns to internalise =====
// - $project early to drop big arrays you do not need downstream (less memory)
// - $project late to shape the final API response
// - Always exclude _id when shipping to clients that don't need it
// - Use $addFields for incremental additions; $project for final shapes
// - Dotted paths work everywhere ('customer.name', 'lines.sku')
// ===== Pitfalls =====
// - Mixing include and exclude in one $project (other than _id) -> error
// - Forgetting that $project drops fields you didn't list
// - Computing twice in different stages -> do it once and reuse with $addFields
// - $project { lines: 0, lines: { qty: 1 } } -> contradictory, errors
Why it matters
$project is the only stage that does shape. Use it to trim before heavy stages so memory stays small, and again at the end to ship a clean API shape. Pair with $addFields when you just want to layer on, not replace.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Reshape documents
{ $project: { name: 1, fullPrice: { $multiply: ['$price', 1.1] } } }
Try it Yourself »
Discussion
Loading…