MongoDB limit() 与 skip() 方法完全解析
(MongoDB 8.0+,2025 年最新实践)
核心结论:
limit(n)= 返回前n条文档skip(n)= 跳过前n条文档
常用于分页,但skip在大数据量时性能差,生产建议用“游标分页”或“范围查询”
一、基本语法
| 方法 | 作用 | 示例 |
|---|---|---|
.limit(n) | 限制返回 最多 n 条 | .limit(10) |
.skip(n) | 跳过前 n 条 | .skip(20) |
// 查询第 3 页,每页 10 条(第 21~30 条)
db.users.find()
.sort({ createdAt: -1 }) // 必须排序!
.skip(20) // 跳过前 20 条
.limit(10) // 返回 10 条
二、典型应用:分页
// 第 page 页,每页 size 条
const page = 3;
const size = 10;
db.posts.find()
.sort({ createdAt: -1 })
.skip((page - 1) * size) // 关键:(page-1)*size
.limit(size)
.pretty()
| 页码 | skip | limit | 返回 |
|---|---|---|---|
| 1 | 0 | 10 | 1~10 |
| 2 | 10 | 10 | 11~20 |
| 3 | 20 | 10 | 21~30 |
三、性能分析:skip 是性能杀手!
| 数据量 | skip(0) | skip(1000) | skip(10000) | skip(100000) |
|---|---|---|---|---|
| 执行时间 | 快 | 稍慢 | 明显变慢 | 极慢(秒级) |
原因:
MongoDB 必须扫描并丢弃前 n 条,即使有索引也无法跳过。
// 坏例子:跳过 10 万条
db.logs.find().skip(100000).limit(10) // 扫描 100,010 条!
// 好例子:用范围查询(推荐)
db.logs.find({ _id: { $gt: lastId } }).limit(10)
四、生产级分页方案(替代 skip)
方案 1:基于 _id 或排序字段的范围查询(推荐)
// 第一页:获取最后一条 _id
const firstPage = db.posts.find()
.sort({ createdAt: -1 })
.limit(10)
.toArray()
const lastId = firstPage[firstPage.length - 1].createdAt
// 第二页:从 lastId 继续
db.posts.find({ createdAt: { $lt: lastId } })
.sort({ createdAt: -1 })
.limit(10)
优点:
- 使用索引,O(log n)
- 支持“下一页”,不支持“上一页”
- 适合无限滚动
方案 2:基于游标的分页(Cursor-based)
// 服务端保存 cursor
let cursor = db.posts.find().sort({ _id: 1 }).batchSize(10)
// 客户端请求下一页
const docs = await cursor.next() // Node.js Driver
优点:
- 零成本翻页
- 支持双向
- 需维护 cursor 状态
方案 3:聚合管道 + $facet(复杂统计分页)
db.posts.aggregate([
{
$facet: {
data: [
{ $match: { status: "published" } },
{ $sort: { createdAt: -1 } },
{ $skip: 20 },
{ $limit: 10 }
],
meta: [
{ $count: "total" }
]
}
}
])
五、limit 与 skip 的注意事项
| 注意点 | 说明 |
|---|---|
必须配合 sort() | 否则分页顺序不一致 |
skip + 大数 = 性能灾难 | 避免 skip(>10000) |
limit(0) = 返回所有 | 等价于无限制 |
skip 可为负数 | 无效,MongoDB 忽略 |
| 游标超时 | 默认 10 分钟,长时间分页需 noCursorTimeout() |
// 安全:防止游标超时
db.collection.find()
.sort({ _id: 1 })
.skip(0)
.limit(10)
.noCursorTimeout()
六、GUI 工具中的 limit 和 skip
| 工具 | 操作 |
|---|---|
| MongoDB Compass | 右侧面板 → Skip / Limit |
| MongoDB Atlas | Query Bar → { $skip: 10, $limit: 10 } |
| VS Code | 查询框支持 .skip().limit() |
七、完整分页脚本(生产可用)
// pagination.js
function paginate(collection, page = 1, size = 10, sort = { createdAt: -1 }) {
const skip = (page - 1) * size
// 方式 1:传统 skip + limit(小数据量)
if (skip < 1000) {
return collection.find()
.sort(sort)
.skip(skip)
.limit(size)
.toArray()
}
// 方式 2:范围查询(大数据量)
const lastDoc = collection.find()
.sort(sort)
.skip(skip)
.limit(1)
.toArray()[0]
if (!lastDoc) return []
const sortField = Object.keys(sort)[0]
const sortValue = lastDoc[sortField]
const operator = sort[sortField] === -1 ? "$lt" : "$gt"
return collection.find({
[sortField]: { [operator]: sortValue }
})
.sort(sort)
.limit(size)
.toArray()
}
// 使用
use blog
const page3 = paginate(db.posts, 3, 10)
page3.forEach(printjson)
八、性能对比测试
// 测试 100万条数据
db.test.insertMany(Array.from({length: 1000000}, (_, i) => ({i, ts: new Date()})))
// 方案 A:skip (慢)
db.test.find().sort({i:1}).skip(500000).limit(10) // ~800ms
// 方案 B:范围查询 (快)
db.test.find({i: {$gt: 500000}}).sort({i:1}).limit(10) // ~2ms
九、一句话总结
**“
limit(n)控制数量,skip(n)控制偏移,但大数据量跳过skip,改用 **范围查询 + 排序字段”
快速模板
// 传统分页(小数据)
.skip((page-1)*10).limit(10)
// 推荐:范围分页
{ createdAt: { $lt: lastSeenDate } }
.sort({ createdAt: -1 })
.limit(10)
// 无限滚动
{ _id: { $gt: lastId } }
.sort({ _id: 1 })
.limit(20)
官方文档:
- https://www.mongodb.com/docs/manual/reference/method/cursor.limit/
- https://www.mongodb.com/docs/manual/reference/method/cursor.skip/
如需 前端分页组件集成、缓存分页结果、从 SQL LIMIT OFFSET 迁移 或 实时分页订阅,欢迎继续提问!