3、聚合函数详解:Count、Sum、Avg、Max、Min、Histogram、Quantile 的实现与选型

聚合函数,说白了就是实时指标系统的「算盘」。你采集上来的数据是散的,但老板想看的是总数、平均值、或者某个百分位。这章我就把几个最常用的聚合函数掰开揉碎了讲,顺便聊聊我踩过的坑。

3.1 基础聚合:Count、Sum、Avg、Max、Min

这几个函数是入门级,但用好了能解决 80% 的场景。我一个个说。

3.1.1 Count:计数

Count 最简单,就是数数。但实时系统里有个坑——去重计数和非去重计数。

  • 非去重 Count:每条数据来一次就加 1。适合统计请求总量、日志条数。
  • 去重 Count:需要维护一个集合,记录哪些 ID 已经算过了。内存开销大,但结果准确。
我的经验:如果业务允许误差,用 HyperLogLog 做近似去重计数。我在统计 UV 时用过,内存从 10MB 降到 1KB,误差不到 2%。

3.1.2 Sum:求和

Sum 就是把所有值加起来。听起来简单,但注意溢出问题。

// 错误示例:int 溢出
int sum = 0;
for (int value : values) {
    sum += value;  // 如果 values 很大,sum 会溢出
}

// 正确做法:使用 long 或 BigInteger
long sum = 0L;
for (int value : values) {
    sum += value;
}

我曾经在统计广告点击金额时,用 int 存 sum,结果一天的数据就溢出了。嗯,从那以后我所有 Sum 都用 long。

3.1.3 Avg:平均值

Avg 的实现有两种方式:

  1. 实时计算:维护 count 和 sum,每次查询时 sum / count。
  2. 滑动窗口:只算最近 N 条数据的平均值。

我建议用第一种,因为简单可靠。滑动窗口的 Avg 容易受窗口大小影响,你想想看,窗口太小波动大,窗口太大反应慢。

3.1.4 Max 和 Min:最大值和最小值

这两个函数实现起来最直接——每次来新数据,跟当前值比一下就行。

// Max 的简单实现
long max = Long.MIN_VALUE;
for (long value : values) {
    if (value > max) {
        max = value;
    }
}

但有个细节:窗口过期。如果数据有生命周期,Max 和 Min 需要支持「退场」。比如一个 5 分钟的窗口,第 1 分钟的最大值在第 6 分钟就该失效了。我见过有人没处理这个,结果 Max 永远只增不减,查了半天才发现。

3.2 高级聚合:Histogram 和 Quantile

基础聚合只能看整体,Histogram 和 Quantile 能看分布。这两个才是实时监控的利器。

3.2.1 Histogram:直方图

Histogram 把数据分成多个桶(bucket),统计每个桶里的数据量。比如统计 API 响应时间:

桶范围 (ms) 计数
0-10 1500
10-50 3200
50-100 800
100-500 200
500+ 50

实现时要注意桶的划分策略:

  • 线性桶:每个桶宽度相同。适合数据分布均匀的场景。
  • 指数桶:桶宽度指数增长。适合长尾分布,比如响应时间。
  • 自定义桶:根据业务需求手动指定边界。
避坑指南:桶的数量不要太多。我曾经设了 1000 个桶,结果内存爆了。一般 10-50 个桶就够用。

3.2.2 Quantile:分位数

Quantile 告诉你「百分之多少的数据小于某个值」。比如 P99 响应时间是 200ms,意思是 99% 的请求都在 200ms 内完成。

实现 Quantile 有两种主流方法:

  1. 基于 Histogram:先算直方图,再根据桶的累计计数估算分位数。
  2. T-Digest:一种近似算法,精度高且内存可控。

我个人习惯用 T-Digest。它在高百分位(P99、P999)的精度特别好,而且内存占用固定。我在监控系统里用过,效果不错。

// T-Digest 伪代码
TDigest digest = new TDigest(100);  // compression 参数控制精度
for (double value : values) {
    digest.add(value);
}
double p99 = digest.quantile(0.99);

3.3 选型指南:什么时候用什么?

选型其实不难,记住这个原则:先看需求,再看资源。

场景 推荐函数 原因
统计请求总量 Count 简单高效,无状态
统计收入总额 Sum 注意用 long 防溢出
监控平均响应时间 Avg 配合滑动窗口使用
发现异常峰值 Max 实时告警首选
分析数据分布 Histogram 桶数量控制在 50 以内
评估服务质量 Quantile (P99) T-Digest 精度高

核心建议:不要一上来就上 Quantile。先跑 Count 和 Avg,等业务成熟了再上 Histogram 和 Quantile。我见过太多人一开始就搞复杂聚合,结果系统扛不住,还得回退。

3.4 知识体系总览

下面这张图把本章的核心逻辑串起来了。你一看就明白。

聚合函数知识体系 聚合函数知识体系 聚合函数 基础聚合 高级聚合 Count Sum Avg Max/Min Histogram Quantile 选型建议 • 80% 场景用基础聚合就够了 • 需要分析分布时用 Histogram • 评估服务质量时用 Quantile (P99)

这张图把基础聚合和高级聚合分成了两条线。左边是 Count、Sum、Avg、Max/Min,右边是 Histogram 和 Quantile。你从根节点往下看,就知道该走哪条路了。

3.5 实战中的坑与技巧

最后分享几个我踩过的坑,希望能帮你省点时间。

  • Count 的并发问题:多线程同时加 Count,记得用原子类(AtomicLong)或者加锁。我见过有人用普通 long,结果计数少了 10%。
  • Sum 的精度问题:浮点数求和会有精度损失。如果对精度要求高,用 BigDecimal 或者转成 long(乘以 100 存分)。
  • Histogram 的桶边界:桶边界要覆盖数据的实际范围。我曾经设了 0-100ms 的桶,结果数据全是 200ms,全进了最后一个桶,直方图完全没意义。
  • Quantile 的冷启动:刚启动时数据少,Quantile 算出来不准。我建议等至少 1000 条数据后再计算。
一个小技巧:如果内存紧张,可以把 Histogram 和 Quantile 的精度调低一点。比如 T-Digest 的 compression 参数从 100 降到 50,内存减半,精度只损失 1-2%。