跳到正文

聚合分析 ​

一句话:ES 聚合是分布式的「分片局部计算 + 协调节点归并」,bucket 负责分组、metric 负责算指标、pipeline 负责对结果再计算;代价是归并依赖近似算法,字段的 doc_values 和内存决定它能跑多大。

适用版本:Elasticsearch 8.x。聚合 DSL 与默认限制以 8.x 官方文档为准。

为什么需要 ​

业务经常要「按状态统计订单数」「按月看销售额」「算 P95 延迟」。这些在关系库里是 GROUP BY + 聚合函数;在 ES 里,数据分散在多个分片,不能简单把各分片的 COUNT DISTINCT 相加。

ES 的做法是:每个分片先在本地聚合,coordinating node 再归并。分组、求和、均值可以精确合并;去重计数和分位数只能用近似算法,用精度换内存和速度。

核心机制 ​

三种聚合 ​

类型作用例子
Bucket把文档分桶terms、date_histogram、range、histogram、composite
Metric在桶内算指标avg、sum、min、max、stats、percentiles、cardinality
Pipeline对聚合结果再计算derivative、cumulative_sum、avg_bucket、bucket_script

聚合可以嵌套:bucket 里面再放 metric 或子 bucket。

一个可运行的 DSL ​

json
GET /orders/_search
{
  "size": 0,
  "query": {
    "range": { "created_at": { "gte": "2026-01-01" } }
  },
  "aggs": {
    "by_status": {
      "terms": { "field": "status", "size": 10 },
      "aggs": {
        "avg_amount": { "avg": { "field": "amount" } },
        "p95_amount": { "percentiles": { "field": "amount", "percents": [95] } },
        "users": { "cardinality": { "field": "user_id" } }
      }
    }
  }
}

要点:

  • size: 0 表示不返回命中文档,只要聚合结果;这是统计查询的默认写法。
  • terms 的 size 是返回的桶数,不是参与聚合的文档数;桶很多时还要看 sum_other_doc_count。
  • cardinality 是近似去重(HyperLogLog++),precision_threshold 默认 3000;精确去重要用 scripted_metric,代价高得多。
  • percentiles 用 TDigest 近似,P95/P99 有误差;不要拿它做财务精确报表。

Pipeline 聚合 ​

json
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "sales_per_month": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month"
      },
      "aggs": {
        "sales": { "sum": { "field": "amount" } },
        "cost":  { "sum": { "field": "cost" } },
        "margin": {
          "bucket_script": {
            "buckets_path": { "s": "sales", "c": "cost" },
            "script": "(params.s - params.c) / params.s"
          }
        },
        "sales_deriv": {
          "derivative": { "buckets_path": "sales" }
        }
      }
    },
    "avg_monthly_sales": {
      "avg_bucket": { "buckets_path": "sales_per_month>sales" }
    }
  }
}
  • bucket_script 是父 pipeline,在同一个桶内对多个 metric 做计算。
  • derivative 计算相邻桶的差值;cumulative_sum 做累计。
  • avg_bucket 是 sibling pipeline,用 > 引用嵌套路径。

分布式归并与近似 ​

  • terms 聚合在每个分片只取前 size 个桶,再归并;shard_size 越大越准,但网络和内存开销越大。
  • 返回结果里的 doc_count_error_upper_bound 是桶计数误差上界;它不为 0 时,说明 terms 结果可能是近似的。
  • search.max_buckets 默认 65536(7.0+),超过会报错;高基数 terms 要么加 filter,要么改用 composite 分页。

doc_values:聚合为什么不用堆 ​

  • keyword、数值、日期、布尔、IP 默认开启 doc_values:列式存储在磁盘上,排序和聚合走它,不占 JVM 堆。
  • text 字段默认没有 doc_values;要对 text 聚合只能开 fielddata,它会把 term 加载到堆里,基数高时极易 OOM。
  • 正确做法:需要聚合/排序的字符串用 keyword(或多字段的 .keyword 子字段),不要对 text 开 fielddata。

最小上手 ​

bash
curl -X PUT "localhost:9200/orders/_doc/1?refresh=true" -H 'Content-Type: application/json' -d '
{"status":"paid","amount":100,"cost":60,"user_id":"u1","created_at":"2026-01-15"}'
curl -X PUT "localhost:9200/orders/_doc/2?refresh=true" -H 'Content-Type: application/json' -d '
{"status":"paid","amount":200,"cost":120,"user_id":"u2","created_at":"2026-02-10"}'

curl -X POST "localhost:9200/orders/_search" -H 'Content-Type: application/json' -d '
{"size":0,
 "aggs":{"by_status":{"terms":{"field":"status"},
   "aggs":{"avg_amount":{"avg":{"field":"amount"}},
           "p95":{"percentiles":{"field":"amount","percents":[95]}}}}}}'

预期返回 by_status.buckets,每个桶里有 doc_count、avg_amount.value 和 p95.values。

生产动作 ​

  • 先过滤再聚合:query 缩小范围,size:0 减少返回;不要把全量数据直接丢进 terms。
  • 控制桶数:terms.size 不要过大;高基数维度用 composite 分页,避免一次生成几十万桶。
  • 看误差:doc_count_error_upper_bound、sum_other_doc_count 不为 0 时,不要把它当精确结果。
  • 字段类型:聚合字段用 keyword/数值/日期;text 聚合先加 .keyword 子字段。
  • 内存:聚合本身走 doc_values,但 terms 的桶、cardinality 的 sketch、fielddata 会占堆;search.max_buckets 是保护线。
  • 慢聚合:用 profile 看是 query 慢还是 aggregation 慢;必要时预聚合到汇总索引。

边界 ​

  • 近似不是精确:cardinality、percentiles 都有误差,财务对账不要用。
  • terms 结果是近似的:分片多、基数高时,doc_count_error_upper_bound 可能很大。
  • text 不适合聚合:开 fielddata 是拿堆换功能,基数高会 OOM。
  • bucket 太多会失败:search.max_buckets 默认 65536,超过直接报错。
  • 聚合不是 OLAP:复杂多表关联、窗口函数、精确去重,交给 ClickHouse/数仓更合适。

讲清楚 ​

ES 聚合分三种:bucket 分组、metric 算指标、pipeline 对结果再算。它在每个分片先本地聚合,再由 coordinating node 归并;求和、均值可以精确合并,去重计数和分位数只能用 HyperLogLog/TDigest 近似,所以 terms 结果会带 doc_count_error_upper_bound。

性能上最关键的是字段类型:keyword/数值/日期默认有 doc_values,聚合走磁盘列存,不占堆;text 没有 doc_values,开 fielddata 会把 term 加载到堆里,基数一高就 OOM。生产上先 filter 再聚合、size:0、控制桶数,高基数维度用 composite 分页;search.max_buckets 默认 65536,是保护线不是目标。

把一门技术讲透到既懂也会用