菜市场打烊后,管理员老肖面前摊着几百张交易小票:谁买了什么、买了多少、花了多少钱。他想知道今天白菜卖了多少斤、哪个品类最受欢迎、全场价格最高的前 10% 的菜贵到哪一档——一张张翻小票?那不现实。Elasticsearch 的聚合查询(Aggregations),就是给搜索引擎配了一位「统计员」:它把原始文档先分筐、再称重、最后拉总账,一条请求输出整份统计报表。这篇文章用老肖的菜市场贯穿全程,带你拆解桶聚合、指标聚合、管道聚合的执行机制,以及 cardinality 去重与 percentiles 分位数的近似算法取舍。

岗位关联

对 AI 从业者而言,聚合查询是日志分析、用户画像、特征统计等数据底座的核心能力;掌握它的执行机制与精度取舍,能让你在造报表、算特征、做监控时又快又准。

目录
  1. 可视化演示
  2. 聚合概览:分类统计台的三件套
  3. 桶聚合成框:terms 是怎么分筐的
  4. 指标聚合称重:avg 和 sum 怎么算账
  5. 嵌套聚合分层:大筐里套小筐
  6. 管道聚合联动:把筐的结果再抄一张表
  7. 基数去重统计:抓一把豆子估算一缸
  8. 分位数分析:找到 90% 的菜的价钱线
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Elasticsearch的桶聚合分桶机制、指标聚合统计计算及嵌套聚合执行顺序与管道后处理流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

terms桶聚合 文档归桶 size限制 Bucket=terms(field,size) 按字段值分桶terms的size参数控制返回桶数影响精度

聚合概览:分类统计台的三件套

菜市场打烊后,管理员老肖面前摊着一摞交易小票。他先摆出几个大筐,贴上「蔬菜」「水果」「粮油」「干货」的标签,然后把小票按品类依次丢进对应的筐。这个「分筐」的动作,就是 ES 里最核心的桶聚合;筐里装了多少张票、总金额多少,则是接下来要讲的指标聚合。

在 Elasticsearch 中,聚合查询把搜索引擎从「查文档」升级为「做统计」。普通查询只负责把满足条件的小票挑出来,聚合则负责把这些小票分门别类、算出报表。两者通常写在同一请求里:先用 query 圈定范围,再用 aggs 做统计。

聚合按功能分成三大类。桶聚合负责把文档分进不同的组,组被称为「桶」;指标聚合负责在桶内计算数值,如求和、平均、最大最小;管道聚合则以其他聚合的输出为输入做二次计算。三者可以自由组合成一个树形结构:最外层是桶,桶里挂指标,指标之上再挂管道。

老肖的统计台从功能上拆成三件套:筐(桶聚合)、秤(指标聚合)、汇总计算表(管道聚合)。理解了这三件套,就理解了 ES 聚合查询的骨架;接下来我们一个个看它们是怎么工作的。

D=⋃k=1KBk,Bi∩Bj=∅ (i≠j)\displaystyle D = \bigcup_{k=1}^{K} B_k, \qquad B_i \cap B_j = \varnothing \ (i \neq j)
一个完整的聚合结果,等于把全部文档划分成互不重叠的若干桶;桶并起来不多不少正好是全部文档。
  1. D 表示全部文档集合聚合先拿到满足查询条件的完整文档集,这是所有统计的原料。
    D={d1,d2,…,dn}\displaystyle D = \{ d_1, d_2, \ldots, d_n \}
  2. B_k 表示第 k 个桶每个桶装着一组拥有相同字段值的文档,如同贴好标签的筐。
    Bk={d∈D:key(d)=k}\displaystyle B_k = \{ d \in D : \text{key}(d) = k \}
  3. 并集覆盖全部文档每个文档必然落入某个桶,不会有人被遗漏在统计之外。
    ⋃k=1KBk=D\displaystyle \bigcup_{k=1}^{K} B_k = D
  4. 交集为空表示互不重叠一个文档只属于一个桶,不会同时出现在两个筐里。
    Bi∩Bj=∅\displaystyle B_i \cap B_j = \varnothing

桶聚合成框:terms 是怎么分筐的

老肖把 300 张小票按品类分筐,发现「蔬菜」的筐最满。terms 桶聚合做的事情一模一样:扫描索引里所有文档,读取指定字段的值,把每个唯一值变成一个桶,并把拥有该值的文档「丢」进去。

桶聚合里最常用的是 terms,它天然适合「分组统计」场景:按品类分组、按状态分组、按城市分组。分组过程中,每个桶会自动记录 doc_count,也就是桶里的文档数,这个数字是后续一切排序和筛选的基础。

这里有一个新手必踩的坑:terms 默认只返回文档数最多的前 10 个桶,其余的唯一值被直接截断,不会合并成「其他」。要返回更多桶,需要显式指定 size;size 越大,内存和耗时越高,所以报表场景里通常会把 size 控制在几十以内。

老肖的台面上永远只摆最热闹的 10 个筐,冷门品类被留在后厨台账里。这个「截断」设计,保证了统计台面不堆满几千个几乎没人买的品类。

doc_count(Bk)=∣{d∈D:terms_key(d)=k}∣\displaystyle \text{doc\_count}(B_k) = |\{ d \in D : \text{terms\_key}(d) = k \}|
terms 的 doc_count,就是「字段值等于某个键的文档有多少个」,用集合筛选取大小的写法表达出来。
  1. terms_key 取分类字段ES 读取每个文档里指定字段的值,作为「分配进哪个筐」的依据。
    terms_key(d)=d.category\displaystyle \text{terms\_key}(d) = d.\text{category}
  2. 集合筛选只留匹配项花括号内定义了一个子集,只保留字段值恰好等于 k 的文档。
    {d∈D:terms_key(d)=k}\displaystyle \{ d \in D : \text{terms\_key}(d) = k \}
  3. 竖线符号表示取大小外层的一对竖线表示「这个集合里有多少元素」,也就是桶内的文档数。
    ∣S∣=count(S)\displaystyle |S| = \text{count}(S)
  4. 得到桶的文档计数doc_count 就是 terms 聚合返回的每个桶的「重量」,排序和 size 截断都依据它。
    doc_count(Bk)=countk\displaystyle \text{doc\_count}(B_k) = \text{count}_k

指标聚合称重:avg 和 sum 怎么算账

筐分好之后,老肖给每个筐贴上价签:总重量(sum)、平均单价(avg)、最重与最轻(min/max)。指标聚合就是这些「筐上计算器」。

指标聚合不产生新的桶,只在已有桶或全量文档上计算数值。最常用的是 sum(求和)与 avg(平均),此外还有 min、max,以及一次返回多项统计的 stats。

avg 的执行机制很巧妙:分布式环境下,每个分片先计算局部 sum 和局部 count,协调节点把所有分片的局部结果汇总——总 sum 除以总 count——得到全局平均。这样既不用把全量文档搬到一台机器,又能保证结果与「先合并再计算」完全一致。sum 同理,只是少了除法这一步。

老肖的分区称重法很简单:各个摊位先各自把卖出的斤两记在本子上,打烊后统一报到总台。总台把斤两相加、再除以摊位数,就是全场的平均销量。ES 的分片协作,就是这个流程的数字化版本。

xˉ=1n∑i=1nxi=sum(x)doc_count(Bk)\displaystyle \bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i = \frac{\text{sum}(x)}{\text{doc\_count}(B_k)}
avg 就是把桶内所有文档的字段值加起来,再除以文档总数。分布式环境下,总 sum 和总 count 由各分片局部求和后汇总得出。
  1. n 是桶内文档总数分子分母中的 n 都来自该桶的 doc_count。
    n=doc_count(Bk)\displaystyle n = \text{doc\_count}(B_k)
  2. x_i 是每个文档的字段值对桶内第 i 个文档取出要求平均的数值字段。
    xi=di.price\displaystyle x_i = d_i.\text{price}
  3. 求和符号把所有值加总Σ 把 n 个 x_i 累加,得到 sum 聚合的原始结果。
    ∑i=1nxi=sum(x)\displaystyle \sum_{i=1}^{n} x_i = \text{sum}(x)
  4. 除以 n 得到平均值总和均匀摊到每个文档上,这就是 avg 输出的含义。
    xˉ=sum(x)n\displaystyle \bar{x} = \frac{\text{sum}(x)}{n}

嵌套聚合分层:大筐里套小筐

光知道「白菜卖了 500 斤」还不够,老肖还想知道这些白菜里,山东产和本地产各占多少。于是他在「白菜」这个大筐里,又摆了「山东」「河北」「本地」几个小筐。

嵌套聚合就是「大筐套小筐」的直接实现。在一个桶聚合内部再挂一个子聚合,子聚合只处理父桶里的文档,整个过程先外层后内层逐级执行。

执行顺序严格自顶向下:先完成外层桶划分,再在每个外层桶内运行内层子聚合,最终返回一个树形 JSON。每个层级既可以是桶也可以是指标,因此能自由组合出「按品类分桶 → 按产地分桶 → 算平均价」的多级报表。嵌套较深时,ES 默认采用深度优先遍历以节省内存;若外层桶数量巨大,可切换为广度优先,先构建本层桶再处理子聚合。

老肖的统计台从一层变两层:大筐套小筐,标签越贴越细。只要筐足够多、层级足够深,再复杂的分类统计也能一层层展开,这正是嵌套聚合的价值。

Bk,j={d∈Bk:inner_key(d)=j},Bk=⋃j=1JBk,j\displaystyle B_{k, j} = \{ d \in B_k : \text{inner\_key}(d) = j \}, \qquad B_k = \bigcup_{j=1}^{J} B_{k, j}
嵌套聚合是在外层桶内再做一次桶划分——外层桶是内层子桶的并集,内层子桶互不重叠。
  1. B_k 是外层桶的文档集内层子聚合的输入不是全量文档,而是父桶筛过的子集。
    Bk={d∈D:outer_key(d)=k}\displaystyle B_k = \{ d \in D : \text{outer\_key}(d) = k \}
  2. inner_key 再取内层字段在父桶内部,再次按另一个字段划分,相当于大筐里摆小筐。
    inner_key(d)=d.origin\displaystyle \text{inner\_key}(d) = d.\text{origin}
  3. 子桶互斥且完整覆盖父桶父桶被拆成一堆互不重叠的小桶,合起来不多不少。
    Bk,j∩Bk,l=∅ (j≠l)\displaystyle B_{k, j} \cap B_{k, l} = \varnothing \ (j \neq l)
  4. 形成树形层级结果有多少父桶,每个父桶下就有多少子桶,最终输出像一棵倒挂的树。
    result={(k,{(j,Bk,j)})}\displaystyle \text{result} = \{ (k, \{ (j, B_{k, j}) \}) \}

管道聚合联动:把筐的结果再抄一张表

分完筐、称完重,老肖把每个筐的平均价抄到一张小表上,然后对这张小表再算一次平均——找出哪些筐的均价高于全场。这个「对小表再统计」的动作,就是管道聚合。

管道聚合不直接处理文档,它把其他聚合的输出当作输入。最常见的场景是:先做 terms 桶,桶里挂 avg 指标,再用 avg_bucket 管道对「所有桶的平均值」再做一次平均。

管道聚合分两类:父管道像 avg_bucket 或 sum_bucket,输入是某个桶内的指标值;兄弟管道像 max_bucket 或 min_bucket,输出是在兄弟桶之间比较的结果。管道的结果会作为新字段挂到响应里,常用来算「占比」「环比」等衍生指标。使用时要注意:输入聚合必须已经返回,因此管道总是写在内层聚合之后。

老肖的小表方法论,让统计从「一筐一筐看」升级为「跨筐比较」。管道聚合就是那张小表,它把散落在各筐的结论收集起来,提炼出更高一层的洞察。

avg_bucket=1K∑k=1Kvk\displaystyle \text{avg\_bucket} = \frac{1}{K} \sum_{k=1}^{K} v_k
avg_bucket 把每个桶的指标值收集起来再算平均,分母是桶的个数,而不是桶内的文档数。
  1. v_k 来自各桶的指标结果管道聚合不吃原始文档,吃的是桶聚合算好的数值。
    vk=avg(Bk)\displaystyle v_k = \text{avg}(B_k)
  2. 对全部桶的指标值求和把各桶的平均价格、平均耗时等收集起来累加。
    ∑k=1Kvk\displaystyle \sum_{k=1}^{K} v_k
  3. 除以桶数 K得到「所有桶指标的平均」,即 avg_bucket 的语义。
    1K∑k=1Kvk\displaystyle \frac{1}{K} \sum_{k=1}^{K} v_k
  4. 与 sum_bucket 对比sum_bucket 只求和不平均,便于计算总量与占比。
    sum_bucket=∑k=1Kvk\displaystyle \text{sum\_bucket} = \sum_{k=1}^{K} v_k

基数去重统计:抓一把豆子估算一缸

老肖想知道今天市场来了多少位顾客。但同一顾客可能买了三次菜,小票上有重复。精确去重需要拿个小本子一个个记下所有顾客 ID,人一多本子就爆了。于是他改用「抓一把豆子估一缸」的思路:看看这一把里有多少颗,按比例推算整缸。

ES 的 cardinality 聚合解决的就是「去重计数」问题。它的特点是:不精确,但足够准,而且内存占用极小。代价和收益都很明确——极低的内存,换来约 95% 以上的精度。

cardinality 采用 HyperLogLog 算法:把每条记录的字段值做哈希,取哈希值的一部分决定它进入哪个「寄存器」,另一部分统计二进制尾部连续 0 的个数。所有寄存器记录各自见过的最大连续 0 个数,最后用调和平均估算出基数。默认配置下,误差约为 1%,而内存只需几 KB,远小于精确去重所需的集合大小。

老肖从不记得每位顾客的名字,只记得「这一把豆子的密度」。HLL 用极小的「采样记忆」把亿级去重压缩到几 KB,这正是海量日志统计里的经典取舍。

n^=αm m2(∑j=1m2−Mj)−1,ε≈1.04m\displaystyle \hat{n} = \alpha_m \, m^2 \left( \sum_{j=1}^{m} 2^{-M_j} \right)^{-1}, \qquad \varepsilon \approx \frac{1.04}{\sqrt{m}}
HLL 用 m 个寄存器的「最长连续零」记录,通过调和平均估算基数;寄存器越多误差越小。
  1. m 是哈希寄存器的个数HLL 把哈希值分成 m 个寄存器,每个寄存器记录一段哈希的特征。
    registers=m\displaystyle \text{registers} = m
  2. M_j 记录最长连续零对落入第 j 个寄存器的哈希值,统计二进制尾部连续 0 的最长长度,记为 M_j。
    Mj=max⁡(leading_zeros(h))\displaystyle M_j = \max(\text{leading\_zeros}(h))
  3. 调和平均估算基数把所有寄存器的 2 的负 M_j 次幂求和再取倒数,得到粗略估计 \hat{n}。
    n^≈αmm2(∑j=1m2−Mj)−1\displaystyle \hat{n} \approx \alpha_m m^2 \left( \sum_{j=1}^{m} 2^{-M_j} \right)^{-1}
  4. 误差随寄存器数下降寄存器越多越精确,1.04/√m 给出相对误差上界,默认配置下约 1% 左右。
    ε≈1.04m\displaystyle \varepsilon \approx \frac{1.04}{\sqrt{m}}

分位数分析:找到 90% 的菜的价钱线

老肖还想知道:「全场 90% 的菜,单价都在多少以下?」这条线就是第 90 百分位。平均价会被几斤进口松露拉高,但分位数只关心「排在最前面的 90% 只到哪一档」,对极端值不敏感。

percentiles 聚合返回指定的分位数,如 p50、p90、p99。它比 avg 更能刻画数据分布形态,是分析长尾问题的标准工具。

percentiles 在内部使用 TDigest 近似算法:把数据压缩成一组带权重的「质心」,用很少的内存描绘出数据分布的大致形状,再根据质心插值出任意分位数。它对长尾分布特别友好——这正是日志耗时、请求延迟这类数据的典型形态。需要注意:分位数是近似值,精度与内存成正比,但默认配置已足够日常监控使用。

老肖把 300 张小票按价格从小到大排成一列,从第 270 张的位置划一条线——这条线就是 p90。分位数让统计从「大概什么水平」进阶到「边界在哪」,这是识别长尾问题的关键视角。

Q^p=x⌈p100×n⌉\displaystyle \hat{Q}_p = x_{\left\lceil \frac{p}{100} \times n \right\rceil}
p 分位数等于把 n 个值排序后,取第 p/100×n 个位置上的值;p90 意味着 90% 的样本小于它。
  1. 先对样本按值排序把桶内所有字段值从小到大排成一列,这是分位数的基础。
    x1≤x2≤⋯≤xn\displaystyle x_1 \le x_2 \le \cdots \le x_n
  2. p/100 是目标比例p90 表示比例 0.9,要找「有 90% 的数据小于它」的那个值。
    r=p100\displaystyle r = \frac{p}{100}
  3. 乘以 n 算出排名位置比例乘以样本总数得到目标位置,向上取整避免小数下标。
    pos=⌈r×n⌉\displaystyle \text{pos} = \left\lceil r \times n \right\rceil
  4. 取该位置的值为分位数排好序后,第 pos 个值就是 p 分位数的近似输出。
    Q^p=xpos\displaystyle \hat{Q}_p = x_{\text{pos}}

AI 应用场景

电商后台的实时销售大盘

阶段:数据准备与特征统计

解决的问题:传统手工 SQL 面对千万级订单,GROUP BY 复杂且响应慢;AI 特征表需要分钟级更新的聚合指标。ES 聚合一次请求同时输出各品类的销售额(sum)、客单价(avg)与订单数,支撑实时大屏。

具体案例:某电商运营每天打开大盘,看到「手机品类今日 GMV 128 万、客单价 3199 元、较昨日 +5%」,背后就是 terms(品类)+ sum(金额)+ avg(金额)的嵌套聚合,老肖的「分筐称重」在线上每天跑几千次。

微服务日志的 p99 延迟排查

阶段:系统监控与模型观测

解决的问题:只靠平均耗时会被慢请求掩盖,无法定位长尾问题;精准定位需要分位数。percentiles 聚合能直接给出 p50/p90/p99,把「边界在哪」变成一条可监控的指标。

具体案例:SRE 在 Kibana 里对「接口耗时」字段做 percentiles 聚合,发现 p99 从 200ms 飙到 800ms,定位到某次发布引入的慢 SQL;再按 error_status 做 terms 桶聚合缩小范围,半小时内完成回滚。

用户画像的日活去重统计

阶段:特征工程与数据治理

解决的问题:精确 COUNT(DISTINCT user_id) 在亿级数据上内存爆炸;cardinality 用 HLL 近似,几百 KB 内存就能算出误差在 1% 范围的日活数,适合特征表每日刷新。

具体案例:推荐系统团队每天统计曝光用户数、点击用户数,用 cardinality 在日志索引上跑,秒级出数;再结合按天分桶看 7 日趋势,判断新策略对活跃用户的影响。

Python 演示

python82 行
# -*- coding: utf-8 -*-"""模拟 ES 聚合查询:terms 桶,avg 指标,嵌套聚合与 HLL 近似去重""" import random  class AggEngine:    """把文档列表当成本地小索引,复刻 ES 聚合的执行思路"""     def __init__(self, docs):        self.docs = docs     def terms(self, field):        buckets = {}        for d in self.docs:            key = d.get(field)            buckets[key] = buckets.get(key, 0) + 1        return sorted(buckets.items(), key=lambda kv: kv[1], reverse=True)     def avg(self, field, metric_field, key):        matched = [d.get(metric_field, 0) for d in self.docs                   if d.get(field) == key]        return sum(matched) / len(matched) if matched else 0.0     def nested(self, outer_field, inner_field):        result = []        for key, _ in self.terms(outer_field):            sub = AggEngine([d for d in self.docs if d.get(outer_field) == key])            result.append((key, sub.terms(inner_field)))        return result     def hll(self, field, buckets=16):        register = [0] * buckets        for d in self.docs:            h = 0            for ch in str(d.get(field)):                h = (h * 31 + ord(ch)) & 0x7FFFFFFF            idx = h % buckets            rest = h // buckets            zeros = 0            while rest and (rest & 1) == 0:                zeros += 1                rest >>= 1            register[idx] = max(register[idx], zeros + 1)        alpha = 0.7213 / (1 + 1.079 / buckets)        raw = alpha * buckets * buckets / sum(2.0 ** (-r) for r in register)        return int(raw + 0.5)  def main():    random.seed(7)    cats = ["手机", "电脑", "平板", "耳机"]    docs = []    for _ in range(300):        price = random.randint(500, 8000)        docs.append({            "cat": random.choice(cats),            "price": price,            "price_bucket": (price // 1000) * 1000,            "uid": random.randint(1, 1000),        })     eng = AggEngine(docs)     print("== terms 桶(按品类分筐) ==")    for key, count in eng.terms("cat"):        avg = eng.avg("cat", "price", key)        print("%s: 文档数=%d, 平均价格=%.1f" % (key, count, avg))     print("== 嵌套聚合(品类 -> 价格段) ==")    for key, sub in eng.nested("cat", "price_bucket"):        print(key, sub[:3])     exact = len({d["uid"] for d in docs})    estim = eng.hll("uid")    print("== cardinality 去重 ==")    print("精确去重=%d, HLL 估算=%d, 误差=%.1f%%" % (        exact, estim, abs(estim - exact) * 100.0 / exact))  if __name__ == "__main__":    main()
目的:用 300 条模拟订单,复刻 ES 的 terms 分桶、avg 指标、嵌套聚合与 HLL 近似去重,输出与真实 Kibana 报表类似的统计结果。
  1. buckets[key] = buckets.get(key, 0) + 1每遇到一个文档就给它所属的桶计数加一,这就是 doc_count 的累加过程。
  2. return sorted(buckets.items(), key=lambda kv: kv[1], reverse=True)按文档数降序排列桶,模拟 terms 默认只取最热门的桶。
  3. matched = [d.get(metric_field, 0) for d in self.docs if d.get(field) == key]先筛出该桶的全部文档,再取数值字段,对应指标聚合「先定位桶再计算」的流程。
  4. h = (h * 31 + ord(ch)) & 0x7FFFFFFF对字符串做哈希,把任意字段值映射成一个整数,这是 HLL 的第一步。
  5. register[idx] = max(register[idx], zeros + 1)每个寄存器只保留「见过的最长连续零」,这就是 HLL 极省内存的秘诀。
  6. raw = alpha * buckets * buckets / sum(2.0 ** (-r) for r in register)用调和平均估算基数,对应公式中的 \(\hat{n}\) 计算。

常见误区

误区「聚合不就是把我需要的数据筛出来吗?那和查询有什么两样?」初学者常以为写一个聚合就能替代过滤条件。

正解查询负责「筛行」,挑出满足条件的文档;聚合负责「归组统计」,把筛出的文档按字段分桶、算指标。比喻:老肖先挑出「今天卖出的所有小票」(查询),再把它们分筐称重(聚合)。两者配合而非替代。

为何常见:因为聚合结果和命中文档在同一个响应里返回,容易让人以为聚合是一种特殊的查询。

误区「我按城市聚合,明明有 100 个城市的订单,为什么只看到 10 个?数据丢了吗?」

正解terms 默认 size=10,只返回文档数最多的前 10 个桶,其余直接被截断,不会合并成「其他」桶。想要更多桶必须显式传 size,并为此支付更高的内存与耗时。

为何常见:人们习惯了 SQL GROUP BY 返回全部分组,忽略了 ES 出于性能考虑的截断设计。

误区「cardinality 就是 COUNT(DISTINCT user_id),数出来的数字应该是准的。」

正解cardinality 使用 HyperLogLog 近似算法,默认精度约 95%(误差约 1%),换来的是极小的内存和极快的速度。若业务要求精确,应改用精确基数聚合并接受高内存,或把字段存到其他存储去重。

为何常见:名字里带「去重计数」,直觉上会类比 SQL,忽略了 ES 面向海量数据的取舍。

误区「我按品类分桶,再按产地分桶,再加一个价格过滤,不都差不多吗?」

正解嵌套聚合不是过滤,而是「分组的再分组」:内层子聚合只在外层桶的文档集上执行,输出是树形层级。过滤条件应写在 query 里,写在聚合内部是对内层分组的再分类。

为何常见:树形 JSON 输出看起来像多层 where,容易用 SQL 思维理解。

误区「用户平均耗时 200ms,p90 应该也差不多吧?用 avg 就够了。」

正解avg 对极端值非常敏感,少量慢请求就能把平均值拉高;而 p90 表示「有 90% 的请求低于该值」,更能刻画真实体验。一两个 10 秒的请求就能让 avg 翻倍,但 p90 几乎不变。

为何常见:日常习惯用「平均」概括集中趋势,低估了长尾对平均值的污染。

面试题

Q1. ES 中负责把文档划分成多个分组的聚合类型是?

单选题 · easy

  • A.分桶聚合← 正确答案
  • B.指标聚合
  • C.管道聚合
  • D.嵌套聚合

答案:A。分桶聚合等于「分筐」,把文档按字段值归入不同组,是聚合的骨架;指标聚合是在筐内算 avg/sum;管道聚合是对各筐结果再统计;嵌套聚合本质仍是桶的层级组合。

Q2. 指标聚合 avg 的计算过程是?

单选题 · easy

  • A.总和除以文档数← 正确答案
  • B.最大值减最小值
  • C.计数取中位数
  • D.按字段值排序

答案:A。avg = sum ÷ doc_count,好比筐的总重量除以筐里的票数。最大值减最小值是值域;计数取中位数与 avg 无关;按字段值排序只是分位数计算的第一步。

Q3. terms 聚合未显式指定 size 时,默认返回多少个桶?

单选题 · easy

  • A.返回 5 个
  • B.返回 10 个← 正确答案
  • C.返回 20 个
  • D.返回 100 个

答案:B。terms 默认 size=10,只保留文档数最多的前 10 个桶,其余直接截断。老肖的台面永远只摆最热闹的 10 个筐。想返回更多桶需显式调大 size,同时付出更高内存开销。

Q4. 嵌套聚合正确的执行顺序是?

单选题 · mid

  • A.先桶后指标← 正确答案
  • B.先指标后桶
  • C.指标桶并行
  • D.只执行内层

答案:A。聚合永远先划分桶,再在桶内计算指标,子聚合也要等父桶划分完成才能执行。先指标后桶或并行执行都不符合 ES 自顶向下的执行模型。

Q5. cardinality 聚合实现近似去重的核心思路是?

单选题 · mid

  • A.哈希抽样估算← 正确答案
  • B.全量精确计数
  • C.冒泡排序统计
  • D.遍历求取平均

答案:A。cardinality 用 HyperLogLog:对字段值哈希后分寄存器,记录最长连续 0 的个数,再用调和平均估算基数,即「抓一把豆子估一缸」。全量精确计数内存太大;排序和平均与去重无关。

Q6. percentiles 聚合最适合分析哪类数据分布?

单选题 · mid

  • A.长尾分布数据← 正确答案
  • B.均匀分布数据
  • C.常量分布数据
  • D.高斯分布数据

答案:A。分位数刻画边界而非集中趋势,对长尾分布尤其友好——几个极端值会拉爆平均值,却几乎不改变 p90。均匀、常量、高斯分布用 avg 就能描述,长尾才是分位数的用武之地。

Q7. 管道聚合的输入来源可以是?

多选题 · mid

  • A.桶聚合的结果← 正确答案
  • B.指标聚合的结果← 正确答案
  • C.原始文档集合
  • D.索引映射字段

答案:AB。管道聚合不直接处理文档,输入是其他聚合的输出——桶聚合的「桶结果」和指标聚合的「数值结果」。它消费的是统计结果,而不是原始文档或索引字段映射。

Q8. 三个桶的 avg 值分别为 10、20、30,管道 avg_bucket 的结果是?

单选题 · hard

  • A.等于 20← 正确答案
  • B.等于 60
  • C.等于 10
  • D.等于 30

答案:A。avg_bucket = (10 + 20 + 30) ÷ 3 = 20。它平均的是「三个桶的指标值」,与各桶文档数无关。选 60 是把求和当成了平均;选 10 或 30 是误取了边界值。

Q9. 关于聚合查询正确的说法有哪些?

多选题 · extreme

  • A.查询负责筛选文档← 正确答案
  • B.聚合负责分组统计← 正确答案
  • C.桶全部被完整返回
  • D.指标可对全局计算← 正确答案

答案:ABD。query 筛行、aggs 统计,两者各司其职;指标聚合既可以挂在桶内,也可以对全量文档直接计算(如全局 avg);terms 默认只返回 10 个桶,「桶全部被完整返回」是常见误解。

Q10. 某 terms 聚合有 100 个唯一值,设置 size=5 后,其余 95 个桶如何处理?

单选题 · hard

  • A.直接被截断丢弃← 正确答案
  • B.自动合并为其他
  • C.转为精确计数
  • D.延迟至下次返回

答案:A。ES 直接丢弃未进入返回列表的桶,不会像 SQL 那样汇总成「其他」,也不会缓存或延迟。这个截断开关正是 terms 控制内存与响应的关键旋钮。