脚本如何分页展示聚合结果

wen 实用脚本 28

本文目录导读:

脚本如何分页展示聚合结果

  1. 方案一:传统 $skip + $limit(简单但效率低)
  2. 方案二:基于游标/上一页最后一条(推荐用于大数据)
  3. 方案三:预计算分页键(适合固定分组)
  4. 方案四:ES 聚合分页(composite 聚合)
  5. 实用建议

分页展示聚合结果是一个常见需求,但直接对聚合结果进行分页(如 $skip$limit)存在性能瓶颈,尤其是数据量大时。

以下是几种主流方案及其实现脚本(以 MongoDBElasticsearch 为例,因为它们最常处理聚合分页):


传统 $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 大数据聚合

实用建议

  1. 对于常见的后台管理系统(数据<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();
  1. 对于用户可见的实时数据(如社交动态):游标分页是最佳选择,前端记录 _id 或时间戳。

  2. 如果需要静态报表(历史数据):预计算分页键可以做到毫秒级跳页。

如果需要针对特定数据库或语言(如 Java、Python、JS)的代码示例,可以告诉我你的技术栈。

抱歉,评论功能暂时关闭!