MongoDB Academy · 课时

Skip 和 Limit:偏移分页

您将使用 skip() 和 limit() 实现传统的页码分页,并衡量其在大型集合上的性能开销。

第 2 / 4 课13 个步骤

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

什么是偏移分页?

偏移分页也称为按页码分页,它会将结果划分为固定大小的页面,并使用页码确定从结果集中的哪个位置开始。第 1 页显示第 1–20 项,第 2 页显示第 21–40 项,依此类推。在 MongoDB 中,这通过 skip() 跳过前面的结果,并通过 limit() 限制每页返回的文档数量来实现。

使用 skip() 和 limit()

limit(N) 会指示游标最多返回 N 个文档。skip(N) 会指示 MongoDB 在返回结果前跳过前 N 个文档。二者结合即可实现按页码分页:要获取每页包含 S 项的第 P 页,请使用 skip((P-1)*S) 和 limit(S)。

const PAGE = 3;
const PAGE_SIZE = 20;

// Page 3 of 20 results per page
const products = await db.collection('products')
  .find({ isActive: true })
  .sort({ createdAt: -1 })
  .skip((PAGE - 1) * PAGE_SIZE)  // skip 40 docs (pages 1 and 2)
  .limit(PAGE_SIZE)               // return next 20
  .toArray();

console.log('Page 3 results:', products.length);

计算总页数

偏移分页通常需要文档总数,以便在用户界面中显示页码。请使用 countDocuments(filter) 在应用分页前统计匹配的文档。将计数操作与分页查询并行执行,以避免增加延迟。总数除以页面大小并向上取整,即可得到总页数。

const filter = { isActive: true };

const [total, results] = await Promise.all([
  db.collection('products').countDocuments(filter),
  db.collection('products')
    .find(filter)
    .sort({ createdAt: -1 })
    .skip((PAGE - 1) * PAGE_SIZE)
    .limit(PAGE_SIZE)
    .toArray()
]);

const totalPages = Math.ceil(total / PAGE_SIZE);
console.log('Total:', total, 'Pages:', totalPages);

skip() 的隐藏成本

MongoDB 通过扫描并丢弃前 N 个文档来实现 skip()。即使使用了索引,MongoDB 也必须遍历并统计 N 个索引条目,然后才能返回结果。第 1 页的 skip 值为 0,因此速度很快;每页 20 项时,第 500 页的 skip 值为 9980,MongoDB 必须遍历近 10,000 个条目,才能找到第 500 页的起始位置。这就是偏移分页的核心性能问题。

O(skip + limit) 查询复杂度

执行 skip/limit 查询所需的时间会随 skip 数量线性增长。查询成本为 O(skip + limit):服务器必须先检查 skip 个文档,然后才能返回 limit 个文档。第 1 页的复杂度为 O(20);每页 20 项时,第 1000 页的复杂度为 O(20020)。随着用户浏览到更高的页码,查询会逐渐变慢,在大型集合中通常会从几毫秒增加到几秒。

何时可以接受偏移分页

尽管存在性能限制,但在以下情况下可以接受偏移分页:(1) 集合中的文档少于几千个;(2) 用户很少浏览前几页以后的页面;(3) 功能要求能够直接跳转到指定页码(例如“跳转到第 47 页”)。许多管理后台和页数较少的搜索结果都符合这一特征。对于无限滚动或大型数据集,请使用键集分页。

使用偏移分页实现 API 端点

典型的 REST 列表端点会接受 page 和 limit 查询参数,对它们进行验证,然后相应地应用 skip/limit。请始终限制最大 limit 值,防止客户端在一次调用中请求数千个文档,从而耗尽服务器内存。

// Express route: GET /api/products?page=2&limit=20
async function listProducts(req, res) {
  const page  = Math.max(1, parseInt(req.query.page)  || 1);
  const limit = Math.min(100, parseInt(req.query.limit) || 20); // cap at 100
  const skip  = (page - 1) * limit;

  const [total, items] = await Promise.all([
    Product.countDocuments({ isActive: true }),
    Product.find({ isActive: true }).sort('-createdAt').skip(skip).limit(limit).lean()
  ]);

  res.json({ page, limit, total, totalPages: Math.ceil(total / limit), items });
}

偏移分页的数据一致性问题

偏移分页存在一个隐蔽的正确性问题:如果在两次页面请求之间插入或删除了文档,项目在排序结果集中的位置就可能发生变化。在第 1 页和第 2 页之间插入一个文档,会使后续所有文档向后移动,导致某个项目同时出现在两个页面中(重复),或者被完全跳过。这称为页面漂移问题,是偏移分页固有的问题。

估算计数与精确计数

对于非常大的集合,countDocuments(filter) 可能会很慢,因为它需要扫描索引。另一种选择是 estimatedDocumentCount(),它的复杂度为 O(1),但会统计集合中的所有文档,而不会应用筛选条件。对于只想获取未经过滤的总数这一类简单场景,估算计数会快得多。对于大型集合中的筛选计数,请考虑缓存计数结果,或使用 Atlas 的分面搜索。

// O(1) but no filter support
const approxTotal = await db.collection('products').estimatedDocumentCount();

// Exact count with filter (slower on large collections)
const exactTotal = await db.collection('products').countDocuments({ isActive: true });

将 skip/limit 与投影结合使用

对于列表端点,请始终将分页与紧凑的投影结合使用。分页时获取所有字段会抵消分页的意义:您仍然需要为每一页传输完整的文档内容。只返回摘要字段(名称、价格、缩略图)的投影,与获取完整文档相比可以减少 80%–95% 的带宽消耗,使分页在较大的偏移量下仍然可行。

const SUMMARY = { _id: 1, name: 1, price: 1, thumbnailUrl: 1, rating: 1 };

const items = await db.collection('products')
  .find({ isActive: true })
  .projection(SUMMARY)
  .sort({ rating: -1 })
  .skip((PAGE - 1) * PAGE_SIZE)
  .limit(PAGE_SIZE)
  .toArray();

何时切换到键集分页

在以下情况下,应从偏移分页切换到键集(游标)分页:用户会无限滚动浏览结果(不需要页码);集合包含超过 100,000 个文档;较高页码的页面加载时间明显增加;或者页面请求之间的数据经常发生变化。无论位于结果集中的哪个位置,键集分页的复杂度始终为 O(log n),因为它会在带索引的字段上使用范围查询,而不是使用 skip。

快速检查

测试您对本课 MongoDB 和 NoSQL 数据库概念的理解。

课程回顾

本课您学到了:偏移分页使用 skip((page-1)*size) 和 limit(size) 获取一个页面;skip() 会扫描并丢弃文档,因此深层页面会逐渐变慢;以及偏移分页适用于小型集合或页数较少的场景,而键集分页在大规模数据下表现更好。接下来,我们将使用范围查询实现键集分页,以获得一致的 O(log n) 性能。

免费开始

用 AI 导师学习 JavaScript — 免费

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

课程
30
课程
120

常见问题解答

「Skip 和 Limit:偏移分页」课时是免费的吗?

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

「Skip 和 Limit:偏移分页」这节课中我会学到什么?

您将使用 skip() 和 limit() 实现传统的页码分页,并衡量其在大型集合上的性能开销。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 MongoDB Academy 需要有经验吗?

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

「Skip 和 Limit:偏移分页」课时需要多长时间?

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

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

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

此课程中的所有课时

  1. 使用 sort() 按多个键排序
  2. Skip 和 Limit:偏移分页
  3. 使用范围查询实现键集分页
  4. 组合排序、Skip、Limit 和投影
← 返回 MongoDB Academy