0Pricing
MongoDB Academy · 강의

$match와 $project: 필터링 및 형태 변경

학습자는 성능을 위해 파이프라인 앞부분에 $match를 배치하고, $project를 사용해 필드 이름을 바꾸며 새 필드를 계산합니다.

$match와 $project: 필터링 및 형태 변경은(는) CoddyKit의 무료 MongoDB Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 MongoDB Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. MongoDB Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

The $match Stage

The $match stage is the aggregation pipeline's equivalent of a find() filter. It accepts the same query syntax—equality checks, comparison operators, logical operators, $regex, $elemMatch—and filters the document stream, dropping documents that don't match. Documents that pass flow to the next stage; documents that don't are discarded.

// $match uses the same syntax as find() filters
db.orders.aggregate([
  { $match: {
    status: 'completed',
    amount: { $gte: 100 },
    createdAt: { $gte: new Date('2024-01-01') }
  }}
]);
// Only orders matching ALL three conditions pass through

Place $match First for Index Use

When $match is the first stage in the pipeline, MongoDB can use an index to satisfy the filter—exactly like a find() query. If $match appears after other stages, the index advantage is lost because the planner only pushes index use to the first stage. This is the single most impactful performance rule in aggregation pipeline design.

// GOOD: $match first -> uses index on userId
db.orders.aggregate([
  { $match: { userId: 'u1', status: 'active' } },  // index used here
  { $group: { _id: '$productId', count: { $sum: 1 } } }
]);

// BAD: $match after $group -> full collection scan
db.orders.aggregate([
  { $group: { _id: '$productId', count: { $sum: 1 } } },
  { $match: { userId: 'u1' } }  // too late for index
]);

Using $match With $text

The $text full-text search operator can only be used inside $match, and only when it is the first pipeline stage. This restriction exists because MongoDB must push the text search to the text index, which happens at the query plan level before any stage transformations. After the $match filters by text, you can project the text score and sort by relevance in subsequent stages.

db.articles.aggregate([
  // $text in $match MUST be the first stage
  { $match: { $text: { $search: 'mongodb aggregation' } } },
  { $addFields: { score: { $meta: 'textScore' } } },
  { $sort: { score: -1 } },
  { $limit: 5 }
]);

The $project Stage

The $project stage reshapes documents: you can include or exclude fields, rename fields, and compute entirely new fields using expression operators. It passes one output document for each input document (unlike $group which collapses many documents into one). A $project stage is often used to trim down documents before expensive stages or to prepare the final output shape.

db.users.aggregate([
  { $project: {
    _id: 0,             // exclude _id
    name: 1,            // include name
    email: 1,           // include email
    // Exclude password - never send to client!
    // (fields not listed are excluded when any field is included)
  }}
]);

Inclusion vs Exclusion in $project

Like find() projections, $project cannot mix inclusion and exclusion in the same stage—except for _id (which can always be explicitly excluded even in an inclusion projection). Set a field to 1 to include it, 0 to exclude it, or an expression to compute and include it. Fields not mentioned in an inclusion projection are dropped.

// Inclusion mode: list what you WANT
db.products.aggregate([{
  $project: {
    _id: 0,   // OK to exclude _id in inclusion mode
    name: 1,
    price: 1
  }
}]);

// Exclusion mode: list what you DON'T want
db.products.aggregate([{
  $project: {
    password: 0,
    __v: 0
  }
}]);
// Cannot mix: { name: 1, password: 0 } is an error (except _id)

Computing New Fields in $project

The real power of $project is computing new derived fields using expression operators. You can rename a field by assigning it a field reference, perform arithmetic, format strings, or use conditionals—all server-side without touching the stored documents. The computed fields only exist in the pipeline output; the underlying documents are unchanged.

db.invoices.aggregate([
  { $project: {
    invoiceNumber: '$_id',         // rename _id to invoiceNumber
    clientName: '$client.name',   // flatten nested field
    subtotal: 1,
    tax: { $multiply: ['$subtotal', 0.08] },        // compute tax
    total: { $add: ['$subtotal', { $multiply: ['$subtotal', 0.08] }] },
    issued: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }
  }}
]);

Renaming and Nesting Fields

You can use $project to restructure the document shape: flatten nested fields to the top level, or group flat fields into a nested sub-document. This is useful for aligning MongoDB output with an API response schema that differs from the stored document structure, without changing how you store the data.

// Document: { firstName: 'Alice', lastName: 'Smith', age: 30 }
// API wants: { name: { first, last }, age }

db.users.aggregate([{
  $project: {
    _id: 0,
    name: {
      first: '$firstName',  // group into nested object
      last: '$lastName'
    },
    age: 1
  }
}]);
// Output: { name: { first: 'Alice', last: 'Smith' }, age: 30 }

Using $project With Arrays

$project supports array expressions to transform or filter arrays within documents. You can use $map to transform each element, $filter to select elements matching a condition, $slice to take a subset, or $arrayElemAt to access a specific index. These operations run server-side on the full array without needing multiple pipeline stages.

db.articles.aggregate([{
  $project: {
    title: 1,
    // Keep only published tags
    activeTags: {
      $filter: {
        input: '$tags',
        as: 'tag',
        cond: { $eq: ['$$tag.active', true] }
      }
    },
    // First author only
    leadAuthor: { $arrayElemAt: ['$authors', 0] },
    // Uppercase each tag name
    upperTags: { $map: { input: '$tags', as: 't', in: { $toUpper: '$$t' } } }
  }
}]);

Multiple $match Stages

You can use multiple $match stages in the same pipeline. A common pattern is to use $match early to leverage an index, then use $group or $project to compute new fields, then use a second $match to filter on those computed values. The first $match benefits from index use; the second filters the computed result set.

db.orders.aggregate([
  // First $match: uses index on userId
  { $match: { userId: 'u1' } },

  // Compute revenue per product
  { $group: { _id: '$productId', revenue: { $sum: '$amount' } } },

  // Second $match: filter computed revenue (no index possible here)
  { $match: { revenue: { $gte: 500 } } },

  { $sort: { revenue: -1 } }
]);

When to Use $addFields Instead of $project

A common frustration with $project is that in inclusion mode, you must list every field you want to keep. If you just want to add new fields without dropping existing ones, use $addFields (or its alias $set) instead. $addFields passes through all existing fields and only adds or overwrites the specified fields—much less verbose for simple computed-field additions.

// $project: must list ALL fields you want to keep
db.users.aggregate([{ $project: { name: 1, email: 1, age: 1,
  ageGroup: { $cond: { if: { $lt: ['$age', 18] }, then: 'minor', else: 'adult' } }
}}]);

// $addFields: keeps all fields, just adds ageGroup
db.users.aggregate([{ $addFields: {
  ageGroup: { $cond: { if: { $lt: ['$age', 18] }, then: 'minor', else: 'adult' } }
}}]);
// All original fields (name, email, age, etc.) are preserved

Pipeline Performance: $project Early

Placing a $project stage early in the pipeline to trim unnecessary fields reduces the memory and bandwidth used by subsequent stages. If later stages don't need a large embedded array or a long text field, exclude them with an early $project or $unset. This is especially impactful when documents are large and the pipeline has many stages.

db.articles.aggregate([
  { $match: { status: 'published' } },

  // Trim large fields early before expensive stages
  { $project: {
    title: 1,
    authorId: 1,
    publishedAt: 1,
    categoryId: 1,
    // Exclude: body (could be 50KB), rawHtml (100KB)
    // This reduces memory in all subsequent stages
  }},

  { $lookup: { from: 'authors', localField: 'authorId', foreignField: '_id', as: 'author' } },
  { $sort: { publishedAt: -1 } },
  { $limit: 20 }
]);

Quick Check

Test your understanding of $match and $project in the aggregation pipeline.

Lesson Recap

In this lesson you learned: $match filters the document stream using the same query syntax as find() and should be placed first to leverage indexes, $project reshapes documents by including, excluding, and computing fields, and $addFields is better when you just want to add fields without listing every existing one. Next up we tackle $group for aggregation and computing totals.

자주 묻는 질문

“$match와 $project: 필터링 및 형태 변경” 강의는 무료인가요?

네 — “$match와 $project: 필터링 및 형태 변경” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 MongoDB Academy 강의 전체를 잠금 해제할 수 있습니다. MongoDB Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“$match와 $project: 필터링 및 형태 변경”에서 뭘 배우나요?

학습자는 성능을 위해 파이프라인 앞부분에 $match를 배치하고, $project를 사용해 필드 이름을 바꾸며 새 필드를 계산합니다. 브라우저에서 직접 실행하는 실습 코드로 MongoDB Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

MongoDB Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 MongoDB Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“$match와 $project: 필터링 및 형태 변경” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 MongoDB Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 MongoDB Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 파이프라인 개념: 단계, 연산자, 표현식
  2. $match와 $project: 필터링 및 형태 변경
  3. $group: 집계 및 합계 계산
  4. 파이프라인에서 $sort, $limit, $skip 사용하기
← MongoDB Academy(으)로 돌아가기