iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

$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…