聚合管道优化技巧
您将重构聚合管道,将 $match 和 $project 尽早前推,避免在 $match 前使用 $unwind,并使用 allowDiskUse 支持大型排序。
聚合管道优化技巧 是 CoddyKit 上的免费 MongoDB Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 MongoDB Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 MongoDB Academy 课程共包含 4 节课。
聚合性能:全局概览
聚合管道可能开销很大——它们可能扫描数百万个文档、构建大型内存结构,并阻塞数秒。让管道运行更快的关键,是尽可能早地应用减少数据量的阶段,使后续阶段处理尽可能小的数据集。MongoDB 还内置了优化器,会自动重新排列某些阶段,但了解手动优化方式可以让您获得最大的控制权。
尽早放置 $match:先过滤,再转换
$match 是 MongoDB 的过滤阶段。尽可能早地将其放在管道中,可以减少流入后续每个阶段的文档数量。如果 $match 能够使用索引,它就会成为速度极快的第一步。即使管道早期的 $match 没有使用索引,也比晚放置更好——这样可以避免处理最终本来就会被丢弃的文档。
// BAD: $group processes all 1M docs, then $match discards most
db.orders.aggregate([
{ $group: { _id: '$status', total: { $sum: '$amount' } } },
{ $match: { _id: 'pending' } } // late match
])
// GOOD: $match first — only 'pending' docs enter $group
db.orders.aggregate([
{ $match: { status: 'pending' } }, // early match, uses index
{ $group: { _id: '$customerId', total: { $sum: '$amount' } } }
])尽早放置 $project:缩小文档大小
尽早使用 $project 删除后续阶段不需要的字段。每个文档包含的字段越少,流经管道的内存表示就越小,从而降低内存压力和 CPU 时间。只有在确定某些字段不再需要时,才应将其投影移除——如果过于激进的早期 $project 删除了后续阶段使用的字段,管道就会失败。
// Drop large, unused fields early
db.products.aggregate([
{ $match: { category: 'electronics' } },
// Remove bulky description and image fields early
{ $project: { name: 1, price: 1, stock: 1 } },
{ $group: { _id: null, avgPrice: { $avg: '$price' } } }
])管道优化器:自动重写
MongoDB 的聚合优化器会在执行管道前自动应用多项重写。主要的自动重写包括:1) 将连续的 $match 阶段合并为一个阶段。2) 在可能的情况下,将 $match 移到 $skip 和 $limit 之前。3) 合并连续的 $limit 阶段。4) 将 $match 推到 $lookup 之前,以便在连接前进行过滤。使用 explain() 查看优化后的管道。
// View the optimizer's rewritten pipeline
db.orders.explain().aggregate([
{ $lookup: { from: 'customers', localField: 'customerId',
foreignField: '_id', as: 'customer' } },
{ $match: { 'customer.country': 'US' } }
])
// Optimizer may push $match before $lookup if fields allow避免在 $match 前使用 $unwind
$unwind 会将数组展开为多个文档——数组中的每个元素对应一个文档。如果将 $match 放在 $unwind 之后,就必须先承担完整的展开成本,再进行过滤。在可能的情况下,应在 $unwind 之前进行过滤,以减少需要展开的数组元素数量。对于大型数组,这可以将文档数量减少一个数量级。
// BAD: unwind first creates N*arraySize docs, then filter
db.blogs.aggregate([
{ $unwind: '$tags' },
{ $match: { tags: 'mongodb' } }
])
// GOOD: filter the root document first, then unwind
db.blogs.aggregate([
{ $match: { tags: 'mongodb' } }, // uses index on tags array
{ $unwind: '$tags' },
{ $match: { tags: 'mongodb' } } // refine after unwind
])$match 和 $sort 的索引覆盖
管道的第一个 $match 阶段可以像 find() 查询一样使用集合索引。第一个 $sort 阶段也可以使用索引来避免内存排序,但前提是它出现在任何会修改文档结构的阶段之前,例如 $project 或 $group。请合理组织管道,使早期的 $match 和 $sort 阶段能够利用索引。
// Index supports $match and $sort at the start
db.events.createIndex({ userId: 1, createdAt: -1 })
db.events.aggregate([
{ $match: { userId: 'u123' } }, // uses index
{ $sort: { createdAt: -1 } }, // uses index sort order
{ $limit: 20 },
{ $project: { _id: 0, type: 1, payload: 1 } }
])对大型排序使用 allowDiskUse
默认情况下,每个聚合阶段最多只能使用 100 MB 的 RAM。如果 $sort、$group 或 $bucket 阶段超过此限制,管道就会因错误而失败。设置 allowDiskUse: true 后,管道可以将数据溢出到磁盘,从而支持任意规模的排序,但代价是 I/O 速度更慢。通常,更合适的解决办法是添加索引,或在排序前进行更积极的过滤。
// Allow spill to disk for large aggregations
db.events.aggregate(
[
{ $match: { year: 2024 } },
{ $sort: { amount: -1 } },
{ $group: { _id: '$region', total: { $sum: '$amount' } } }
],
{ allowDiskUse: true }
)$lookup 性能:连接前先过滤
$lookup 相当于 SQL 中的 JOIN,也是开销最大的阶段之一。要尽量降低成本:1) 将 $match 放在 $lookup 之前,以减少需要连接的文档数量。2) 确保被连接的集合在 foreignField 上建有索引。3) 使用 $lookup 的管道形式,并在其中放置 $match,以便立即过滤连接结果。
// Ensure foreignField is indexed
db.customers.createIndex({ _id: 1 }) // usually already indexed
// Use pipeline $lookup with internal $match to reduce joined docs
db.orders.aggregate([
{ $match: { status: 'pending' } },
{ $lookup: {
from: 'customers',
let: { cid: '$customerId' },
pipeline: [
{ $match: { $expr: { $eq: ['$_id', '$$cid'] } } },
{ $project: { name: 1, email: 1 } } // trim early
],
as: 'customer'
}}
])在分页管道中尽早使用 $limit
构建分页列表接口时,应在过滤和排序之后尽可能早地放置 $limit。如果一个管道先让 100,000 个文档经过 $lookup 和 $addFields,再限制为 20 个文档,其成本会是先执行限制的管道的 5,000 倍。使用范围过滤器的键集分页通常可以完全避免使用 $skip。
// Efficient pagination: limit BEFORE expensive stages
db.products.aggregate([
{ $match: { category: 'books' } },
{ $sort: { rating: -1 } },
{ $limit: 20 }, // limit early!
{ $lookup: { from: 'reviews',
localField: '_id', foreignField: 'productId', as: 'reviews' } }
])使用物化视图预先计算
对于为仪表板或报告提供数据的高开销聚合,请考虑使用 $merge 或 $out 创建物化视图。按计划运行高开销聚合,例如每小时运行一次,并将结果写入专用集合。针对物化视图的查询是成本低廉的定点查找,而不是成本高昂的管道扫描。Atlas 还支持通过触发器创建按需物化视图。
// Materialise daily revenue summary
db.orders.aggregate([
{ $match: { createdAt: { $gte: ISODate('2025-01-01') } } },
{ $group: { _id: '$region', revenue: { $sum: '$amount' } } },
{ $merge: {
into: 'revenue_summary',
whenMatched: 'replace',
whenNotMatched: 'insert'
}}
])分析聚合管道
对聚合操作使用 .explain('executionStats'),查看每个阶段输出了多少文档。查找 nReturned 大幅下降的阶段——真正的工作就发生在那里。如果某个阶段仍在处理数百万个文档,请在它之前添加 $match 或改进索引。对于 Atlas,查询分析器还会显示一段时间内的聚合管道性能。
db.orders.explain('executionStats').aggregate([
{ $match: { status: 'shipped' } },
{ $group: { _id: '$region', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } },
{ $limit: 10 }
])
// Check: nReturned per stage, executionTimeMillisEstimate快速检查
测试您对本课 MongoDB 和 NoSQL 数据库概念的理解。
课程回顾
本课中您学习了:尽可能早地放置 $match 和 $project,以尽量减少后续阶段中的文档数量;确保第一个 $match 和 $sort 使用索引,以避免集合扫描和内存排序;以及对于无法避免的大型排序使用 allowDiskUse,但应优先通过更好的索引或更早的过滤来彻底解决问题。接下来,我们将探索 Atlas Data Federation。
用 AI 导师学习 JavaScript — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 30
- 课程
- 120
常见问题解答
「聚合管道优化技巧」课时是免费的吗?
是的 — 「聚合管道优化技巧」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 MongoDB Academy 课程的其余内容,请升级到 CoddyKit PRO。 MongoDB Academy 课程共包含 4 节课。
「聚合管道优化技巧」这节课中我会学到什么?
您将重构聚合管道,将 $match 和 $project 尽早前推,避免在 $match 前使用 $unwind,并使用 allowDiskUse 支持大型排序。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 MongoDB Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 MongoDB Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「聚合管道优化技巧」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 MongoDB Academy 课中编写并运行代码吗?
能。每节 MongoDB Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。