MongoDB Academy · 课时

管道中的 $sort、$limit 和 $skip

您将对聚合结果进行排序和分页,并了解优化器在 $group 前推送 $match 和 $sort 的规则。

第 4 / 4 课13 个步骤

管道中的 $sort、$limit 和 $skip 是 CoddyKit 上的免费 MongoDB Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 MongoDB Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 MongoDB Academy 课程共包含 4 节课。

概览:结果排序与分页

聚合管道中的三个阶段控制结果的顺序和数量:$sort按照一个或多个字段对文档排序,$limit仅保留前 N 个文档,$skip丢弃前 N 个文档。三者结合即可实现排序和分页。了解应将它们放在管道中的什么位置,会对性能产生重大影响。

$sort 阶段

$sort阶段根据一个或多个字段的值对文档排序。使用 1表示升序,使用 -1表示降序。如果指定了多个字段,文档会先按第一个字段排序;对于第一个字段值相同的文档,再按第二个字段排序,依此类推——这与 SQL 中的多键排序或 MongoDB 的 find().sort() 完全相同。

// Sort by revenue descending, then alphabetically by name for ties
db.products.aggregate([
  { $group: { _id: '$category', revenue: { $sum: '$price' } } },
  { $sort: { revenue: -1, _id: 1 } }  // revenue desc, category name asc
]);

$sort 与索引的使用

当 $sort 是管道的第一个阶段(或紧跟在使用索引的 $match 之后)时,MongoDB 可以使用索引来完成排序,从而完全避免内存排序。当 $sort 出现在会生成新字段的阶段(例如 $group 或 $project)之后时,就没有可用索引,MongoDB 会执行内存排序。大型内存排序(>100 MB)需要设置 allowDiskUse: true。

// Index-backed sort: $sort on indexed field before any transformation
db.orders.aggregate([
  { $match: { userId: 'u1' } },
  { $sort: { createdAt: -1 } }  // if createdAt is indexed, no in-memory sort
]);

// In-memory sort: $sort after $group (computed field, no index)
db.orders.aggregate([
  { $group: { _id: '$userId', total: { $sum: '$amount' } } },
  { $sort: { total: -1 } }  // in-memory: 'total' is a computed field
]);

$limit 阶段

$limit只向后续阶段传递前 N 个文档,并丢弃其余文档。它的用法很简单:传入一个整数,指定允许通过的最大文档数。将 $limit 放在 $lookup 等耗时阶段之前,可以大幅减少执行的连接或查找次数。请始终问自己:“我能否尽早限制数量?”

// Top 10 best-selling products
db.orders.aggregate([
  { $group: { _id: '$productId', sold: { $sum: '$qty' } } },
  { $sort: { sold: -1 } },
  { $limit: 10 },  // only top 10 pass through
  // Only 10 lookups instead of thousands:
  { $lookup: { from: 'products', localField: '_id', foreignField: '_id', as: 'product' } }
]);

$skip 阶段

$skip会从数据流中丢弃前 N 个文档。它相当于聚合中的偏移分页:第 2 页从 skip 10 开始(假设每页限制 10 条)。为了获得一致的结果,$skip 始终应放在 $sort 之后——跳过未排序的文档会导致页面内容不可预测。与 SQL 的 OFFSET 一样,在较大的偏移量下使用 $skip 会很慢,因为 MongoDB 仍然必须处理所有被跳过的文档。

const PAGE = 2;
const PAGE_SIZE = 10;

db.articles.aggregate([
  { $match: { status: 'published' } },
  { $sort: { publishedAt: -1 } },
  { $skip: (PAGE - 1) * PAGE_SIZE },  // skip page 1's 10 docs
  { $limit: PAGE_SIZE }               // take page 2's 10 docs
]);

优化器:合并 $sort + $limit

MongoDB 的聚合优化器会检测 $sort 阶段后是否紧跟着 $limit 阶段,并将它们合并为取前 N 个结果的排序。MongoDB 不会先对所有文档排序再丢弃大部分文档,而是在排序过程中使用优先队列,仅保留排名靠前的 N 个候选项。这样使用的是 O(N) 内存,而不是 O(total) 内存。此优化会自动执行——只需编写先 $sort 后 $limit 的管道即可。

// This pattern triggers the top-N sort optimisation
db.reviews.aggregate([
  { $match: { productId: 'p1' } },
  { $sort: { rating: -1, helpful: -1 } },  // sort
  { $limit: 5 }   // $sort + $limit merged into top-5 sort internally
]);
// MongoDB never sorts ALL reviews; it tracks only the best 5

优化器:调整 $match 和 $sort 的顺序

聚合优化器还会在安全的情况下,自动将 $match 阶段提前到管道中的更前位置。例如,如果 $sort 位于 $match 之前,且排序所依据的字段并非由 $sort 计算得出,优化器就会将 $match 移到 $sort 之前,以减少需要排序的文档数量。您可以在 explain() 输出中查看优化器所做的更改。

// You write:
db.orders.aggregate([
  { $sort: { amount: -1 } },
  { $match: { status: 'completed' } }  // optimizer moves this BEFORE $sort
]);

// Optimized execution (equivalent to):
db.orders.aggregate([
  { $match: { status: 'completed' } },  // fewer docs to sort
  { $sort: { amount: -1 } }
]);

组合全部四个阶段:完整的列表端点

一个现实中的 API 端点通常需要列出带有筛选、排序和分页的产品,并按正确顺序组合 $match、$sort、$skip 和 $limit。顺序始终是:筛选 → 排序 → 跳过 → 限制。这样可以确保 MongoDB 在丢弃不需要的文档之前,利用索引完成筛选,并在可能的情况下完成排序。

async function getProducts({ category, sort = 'price', page = 1, pageSize = 20 }) {
  return db.products.aggregate([
    { $match: { category, inStock: true } },   // 1. filter
    { $sort: { [sort]: 1 } },                  // 2. sort
    { $skip: (page - 1) * pageSize },          // 3. offset
    { $limit: pageSize }                       // 4. take page
  ]).toArray();
}

$skip 在深层偏移量下的性能

$skip 的根本问题在于,MongoDB 仍然必须处理并丢弃每一个被跳过的文档。以每页 20 条为例,在第 1000 页,MongoDB 会先丢弃 19,980 个文档,然后返回 20 个文档。页码越深,速度就越慢。对于面向公众的搜索或信息流 API,如果用户很少浏览到第 5~10 页以后的内容,偏移分页就足够了。对于高频的深层分页,则必须使用键集分页。

// Page 1000 is SLOW: MongoDB processes and discards 19,980 docs
db.posts.aggregate([
  { $sort: { createdAt: -1 } },
  { $skip: 19980 },  // 999 * 20
  { $limit: 20 }
]);

// Keyset pagination: always fast regardless of page depth
db.posts.aggregate([
  { $match: { createdAt: { $lt: lastSeenTimestamp } } },  // cursor
  { $sort: { createdAt: -1 } },
  { $limit: 20 }
]);

$sort 内存限制与 allowDiskUse

聚合管道中的内存 $sort 默认限制为100 MB。如果排序超过此限制,管道会因 QueryExceededMemoryLimitNoDiskUseAllowed 错误而失败。将 { allowDiskUse: true } 作为第二个参数传递给 aggregate(),即可启用溢出到磁盘的功能。或者,尽早应用 $match,或使用支持排序的索引,以减少排序输入的大小。

// Enable disk use for large sorts
db.events.aggregate(
  [
    { $match: { year: 2024 } },
    { $sort: { timestamp: -1 } },
    { $group: { _id: '$userId', events: { $push: '$type' } } }
  ],
  { allowDiskUse: true }  // allows spilling to disk
);

同时获取结果总数

一种常见的 API 模式是在一次数据库往返中同时返回分页结果和总数。使用$facet将管道拆分为两个并行分支:一个用于 $skip/$limit 结果,另一个用于 $count。这样可以避免两次单独的聚合调用,而且比单独统计所有文档更高效。

db.products.aggregate([
  { $match: { category: 'electronics' } },
  { $sort: { price: 1 } },
  { $facet: {
    // Branch 1: paginated results
    data: [
      { $skip: 0 },
      { $limit: 20 }
    ],
    // Branch 2: total count
    total: [
      { $count: 'count' }
    ]
  }}
]);
// Output: { data: [...20 docs...], total: [{ count: 348 }] }

快速检查

测试您对聚合管道中 $sort、$limit 和 $skip 的理解。

课程回顾

在本课中,您学习了:$sort 会对文档排序,放在管道前部时可以受益于索引;$limit 会取前 N 个文档,并能与 $sort 合并为高效的前 N 项排序;$skip 可以实现偏移分页,但在深层偏移量下性能会下降。接下来,我们将学习包括 $lookup 在内的高级聚合阶段,用于连接集合。

免费开始

用 AI 导师学习 JavaScript — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「管道中的 $sort、$limit 和 $skip」课时是免费的吗?

是的 — 「管道中的 $sort、$limit 和 $skip」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 MongoDB Academy 课程的其余内容,请升级到 CoddyKit PRO。 MongoDB Academy 课程共包含 4 节课。

「管道中的 $sort、$limit 和 $skip」这节课中我会学到什么?

您将对聚合结果进行排序和分页,并了解优化器在 $group 前推送 $match 和 $sort 的规则。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 MongoDB Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 MongoDB Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「管道中的 $sort、$limit 和 $skip」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 MongoDB Academy 课中编写并运行代码吗?

能。每节 MongoDB Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 管道概念:阶段、运算符和表达式
  2. $match 和 $project:筛选与重塑
  3. $group:聚合并计算总计
  4. 管道中的 $sort、$limit 和 $skip
← 返回 MongoDB Academy