主题
聚合分析
一句话: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,是保护线不是目标。