$match 和 $project:筛选与重塑
您将把 $match 放在管道前部以提升性能,并使用 $project 重命名字段及计算新字段。
$match 和 $project:筛选与重塑 是 CoddyKit 上的免费 MongoDB Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 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 throughPlace $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 preservedPipeline 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:筛选与重塑」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 MongoDB Academy 课程的其余内容,请升级到 CoddyKit PRO。 MongoDB Academy 课程共包含 4 节课。
「$match 和 $project:筛选与重塑」这节课中我会学到什么?
您将把 $match 放在管道前部以提升性能,并使用 $project 重命名字段及计算新字段。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 MongoDB Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 MongoDB Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「$match 和 $project:筛选与重塑」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 MongoDB Academy 课中编写并运行代码吗?
能。每节 MongoDB Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 管道概念:阶段、运算符和表达式
- $match 和 $project:筛选与重塑
- $group:聚合并计算总计
- 管道中的 $sort、$limit 和 $skip