本文目录导读:

- 方案一:传统
$skip+$limit(简单但效率低) - 方案二:基于游标/上一页最后一条(推荐用于大数据)
- 方案三:预计算分页键(适合固定分组)
- 方案四:ES 聚合分页(
composite聚合) - 实用建议
分页展示聚合结果是一个常见需求,但直接对聚合结果进行分页(如 $skip 和 $limit)存在性能瓶颈,尤其是数据量大时。
以下是几种主流方案及其实现脚本(以 MongoDB 和 Elasticsearch 为例,因为它们最常处理聚合分页):
传统 $skip + $limit(简单但效率低)
适用场景:数据量小(<1万条)、页码跳转频繁。
// MongoDB Aggregation Pipeline
db.collection.aggregate([
{ $match: { status: "active" } },
{ $group: { _id: "$category", count: { $sum: 1 }, totalAmount: { $sum: "$amount" } } },
{ $sort: { count: -1 } },
{ $skip: (page - 1) * pageSize }, // 跳过的文档数
{ $limit: pageSize } // 每页返回数
])
缺点:
- 页码越靠后,
$skip越慢(需要遍历并丢弃大量数据)。 - 无法获取总页数(需额外执行
$count阶段)。
基于游标/上一页最后一条(推荐用于大数据)
适用场景:无限滚动、实时数据流、数据量大(千万级)。
原理:记住上一页最后一个分组的 _id 或排序字段,下一页直接从该位置开始。
// 假设按 count 降序排列,_id 是分组键
// 第一页:获取前 20 条
let lastDoc = null;
function nextPage(lastDoc) {
let matchStage = {};
if (lastDoc) {
// 游标条件:count 小于上一页最后一条 或 等于且 _id 更大(处理并列)
matchStage = {
$match: {
$or: [
{ count: { $lt: lastDoc.count } },
{ count: lastDoc.count, _id: { $gt: lastDoc._id } }
]
}
};
}
return db.collection.aggregate([
{ $match: { status: "active" } },
{ $group: { _id: "$category", count: { $sum: 1 }, totalAmount: { $sum: "$amount" } } },
{ $sort: { count: -1, _id: 1 } },
...(matchStage ? [matchStage] : []), // 条件过滤
{ $limit: 20 }
]);
}
// 使用:
let page1 = await nextPage(null);
let page2 = await nextPage(page1[page1.length - 1]);
优点:
- 稳定高效,不随页码增长变慢。
- 适合实时数据(新数据不会导致旧数据偏移)。
缺点:
- 无法直接跳转到任意页码(页码无意义)。
- 需要前端配合保存游标。
预计算分页键(适合固定分组)
适用场景:分组数量固定且较少(如省市、月份),且需要快速跳页。
思路:将所有分组键存入一个数组,用数组索引模拟页码。
// 1. 先执行一次聚合,只提取分组键
let allGroups = await db.collection.aggregate([
{ $group: { _id: "$department" } },
{ $sort: { _id: 1 } },
{ $group: { _id: null, keys: { $push: "$_id" } } }
]).toArray();
let totalPages = Math.ceil(allGroups[0].keys.length / pageSize);
// 2. 根据页码取对应的分组键
let start = (page - 1) * pageSize;
let end = start + pageSize;
let pageKeys = allGroups[0].keys.slice(start, end);
// 3. 用这些键再次聚合(精准过滤)
let pageData = await db.collection.aggregate([
{ $match: { department: { $in: pageKeys } } },
{ $group: { _id: "$department", totalSales: { $sum: "$amount" } } },
{ $sort: { _id: 1 } }
]).toArray();
优点:
- 任意页码直接跳转。
- 聚合速度快(只处理少量分组)。
缺点:
- 需要两次查询(获取键 + 获取数据),但键查询极快。
- 如果分组数量本身巨大(如百万个),不适用。
ES 聚合分页(composite 聚合)
Elasticsearch 提供了原生支持游标分页的 composite 聚合。
脚本示例(Python + elasticsearch-py):
from elasticsearch import Elasticsearch
es = Elasticsearch()
# 第一页
page1 = es.search(index="orders", body={
"size": 0, # 不返回原始文档
"aggs": {
"my_composite": {
"composite": {
"size": 20, # 每页20个桶
"sources": [
{ "category": { "terms": { "field": "category.keyword" } } }
]
},
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
})
# 获取下一页的 after_key
after_key = page1['aggregations']['my_composite']['after_key']
# 下一页
page2 = es.search(index="orders", body={
"size": 0,
"aggs": {
"my_composite": {
"composite": {
"size": 20,
"sources": [{"category": {"terms": {"field": "category.keyword"}}}],
"after": after_key # 关键:传入上一页的 after_key
},
"aggs": {"avg_price": {"avg": {"field": "price"}}}
}
}
})
| 方法 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
$skip+$limit |
实现简单,支持任意页码 | 深度分页极慢 | 小数据、后台管理 |
| 游标分页 | 高效,稳定 | 不能跳页 | 无限滚动、API |
| 预计算键 | 快速跳页 | 两次查询,分组数少 | 分组固定(100以内) |
| ES composite | ES原生支持,高效 | 仅限于ES | 大数据聚合 |
实用建议
- 对于常见的后台管理系统(数据<10万):用
$skip+$limit完全够用,配合$facet一次性返回总条数:
let result = await db.collection.aggregate([
{ $match: { ... } },
{ $group: { _id: "$category", count: { $sum: 1 } } },
{ $facet: {
total: [{ $count: "count" }],
data: [{ $sort: { count: -1 } }, { $skip: skip }, { $limit: pageSize }]
}}
]).toArray();
-
对于用户可见的实时数据(如社交动态):游标分页是最佳选择,前端记录
_id或时间戳。 -
如果需要静态报表(历史数据):预计算分页键可以做到毫秒级跳页。
如果需要针对特定数据库或语言(如 Java、Python、JS)的代码示例,可以告诉我你的技术栈。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播