尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Elasticsearch 指标聚合详解:cardinality、stats、percentiles 与 string_stats
做 Elasticsearch 聚合查询很多人一上来就盯着桶聚合terms 一把梭等到需要去重计数、基础统计、百分位、字符串统计这些指标聚合的时候反而不知道怎么下手。这篇是 Elasticsearch 笔记系列的第四篇专门把四种指标聚合一次讲透包含 DSL 写法、返回结果解读、底层原理和踩坑记录。如果你已经会写基本的 match、term、range 查询也用过 terms 和 date_histogram 做分组那这篇正好帮你补齐指标聚合这块拼图。新手也不用担心每段都有可以直接抄的请求体拿真实索引换一下字段名就能跑。1. 先弄明白这四种聚合属于哪一类1.1 指标聚合和桶聚合别在这个地方绕晕Elasticsearch 的聚合框架整体上分三大类桶聚合Bucket Aggregation、指标聚合Metric Aggregation、管道聚合Pipeline Aggregation。很多人一开始分不清前两者其实记住一句话就行桶聚合负责把文档分到不同的“组”里它本身不产出统计值指标聚合负责对一组文档计算“数”输出一个数值或者一个结构体。常见的 terms、range、date_histogram、filter 聚合都属于桶聚合它们的结果是一个个 bucket每个 bucket 里有文档数 doc_count。而 avg、sum、max、min 是大家最熟悉的指标聚合本篇要讲的去重计数cardinality、基础统计stats / extended_stats、百分位percentiles / percentile_ranks、字符串统计string_stats也都是指标聚合且比那几个老面孔要实用得多。还有一个容易忽略的点指标聚合可以单独直接跑在整个索引范围上也可以嵌在桶聚合里面作为子聚合。实际业务里后者才是主流玩法比如先按天分桶再在每个桶里统计 UV这种组合能力才是聚合查询真正值钱的地方。1.2 四个聚合各自解决什么问题这四个指标聚合看上去都是“算数”但适用场景差异很大。先列一张总表后面逐个说。聚合类型典型业务问题主要返回cardinality 去重计数这个接口有多少个不同用户调用活动页 UV 是多少一个数值stats / extended_stats 基础统计订单金额均值多少、总量多少、方差标准差多大多个数值percentiles / percentile_ranks 百分位接口 P95、P99 是多少超过 500ms 的请求占比多少多个数值string_stats 字符串统计日志里 message 字段长度分布如何字符信息熵是多少多个数值这四个聚合里cardinality 和 percentiles 属于近似聚合底层用了专门的算法来换内存和速度stats 是精确计算string_stats 则比较冷门但处理文本类分析时非常好用。2. 去重计数cardinality 聚合算 UV 的好帮手2.1 基础语法与返回结果假设有一个订单索引 order_infomapping 里 user_id 是 keyword 类型。现在要统计这个索引里一共有多少个不同的用户下过单DSL 非常简单GET /order_info/_search { size: 0, aggs: { distinct_users: { cardinality: { field: user_id } } } }返回结果长这样{ took: 12, timed_out: false, hits: { total: { value: 100000, relation: eq }, max_score: null }, aggregations: { distinct_users: { value: 8234 } } }注意一点hits.total 是命中的文档总数 100000而 aggregations.distinct_users.value 是去重后的用户数 8234。这两者千万别搞混尤其是面试的时候很容易被问到。如果 user_id 是 long 类型写法完全一样。但如果 user_id 是 text 类型直接聚合大概率会报错这时候需要用到 keyword 子字段也就是把 field 写成 user_id.keyword。这个坑后面排错部分会专门讲。2.2 precision_threshold精度和内存的权衡cardinality 底层用的是 HyperLogLogHLL算法。这个算法的核心是通过概率统计的方式估算集合里不重复元素的个数不需要把所有去重值全部记下来所以内存占用非常低。但估算就存在误差于是 Elasticsearch 给了一个开关参数 precision_threshold。precision_threshold 的默认值是 3000最大值是 40000。当去重后的基数小于等于这个阈值时Elasticsearch 返回的是精确值一旦超过阈值就开始用 HLL 做估算。这个设计很聪明小基数场景下精确计算成本不高直接算大基数场景下你大概率只需要一个量级正确的结果毕竟几百万和几百万零几千对于多数业务来说差别可以接受。参数设置的原则其实就一句话如果你明确知道去重规模不会特别大而且业务上对精度敏感可以把这个阈值调高一些如果去重规模本身已经几十万上百万阈值调得再高也拦不住估算只会白白增加内存压力。我见过有人把索引里所有 cardinality 聚合的 threshold 无脑设成 40000结果集群内存使用率明显上涨。这类全局配置真不建议乱来先按默认值跑观察真实基数后再决定要不要调。2.3 实际用的时候要注意什么第一cardinality 是近似聚合这个事实要提前跟业务方讲清楚。有些运营同学拿 UV 数对口径一旦数据量上来数值每天有轻微抖动很容易被质疑“数据是不是有问题”。我的处理方式是把 precision_threshold 按数据规模设到一个合理水平跟业务对齐“基数超过阈值后为估算值”这个前提省得后续扯皮。第二字段缺失值会影响统计语义。默认情况下聚合会忽略没有 user_id 的文档。如果文档里存在大量空值聚合结果的“去重用户数”其实是“有 user_id 的用户数”这个口径本身没有错但很容易被误解。如果确实要把缺失值纳入统计可以加 missing 参数GET /order_info/_search { size: 0, aggs: { distinct_users: { cardinality: { field: user_id, missing: UNKNOWN_USER } } } }我自己的习惯是统计 UV 前先在 query 层加一个 exists 过滤把没有 user_id 的文档排除掉语义更干净。第三cardinality 也支持 script但能不用就不用。脚本聚合性能开销远高于普通字段聚合遇到大数据量容易成为集群瓶颈。真需要做字段拼接、格式化后再去重建议在写入阶段提前处理好而不是在查询时用 script 现算。3. 基础统计stats 与 extended_stats一个顶五个3.1 stats 能给你哪些指标stats 聚合是一个复合指标聚合一次调用返回 count、min、max、avg、sum 五个指标。它非常适合快速摸清一个字段的整体分布比如订单金额。GET /order_info/_search { size: 0, aggs: { amount_stats: { stats: { field: amount } } } }返回结果{ aggregations: { amount_stats: { count: 95000, min: 1.0, max: 9999.0, avg: 268.35, sum: 25493250.0 } } }如果你的业务只是想知道“大概多少钱”到这里就够了。但有两个细节要提醒一下count 表示的是参与统计的、该字段有值的文档数不是查询命中的总文档数min 和 max 是极值极值对异常数据非常敏感所以看到极端大或极端小的数值时不要急着下结论先去查一下数据质量。3.2 extended_stats 扩展出的方差和标准差如果只看均值不够要了解数据波动那就得用 extended_stats。它会在 stats 的基础上额外返回 sum_of_squares平方和、variance方差、std_deviation标准差以及基于标准差自动计算的上下边界。GET /order_info/_search { size: 0, aggs: { amount_ext_stats: { extended_stats: { field: amount } } } }返回结果会多出下面这组字段{ aggregations: { amount_ext_stats: { count: 95000, min: 1.0, max: 9999.0, avg: 268.35, sum: 25493250.0, sum_of_squares: 684123456.0, variance: 102400.0, variance_population: 102400.0, variance_sampling: 102412.3, std_deviation: 320.0, std_deviation_population: 320.0, std_deviation_sampling: 320.06, std_deviation_bounds: { upper: 908.35, lower: -371.65, upper_population: 908.35, lower_population: -371.65, upper_sampling: 908.47, lower_sampling: -371.77 } } } }这里有两个地方值得展开讲。首先是 population 和 sampling 的区别。Elasticsearch 返回的方差、标准差都会给总体和样本两套值区别在于分母是 n 还是 n-1。绝大多数业务分析场景直接用 population 那套就可以只有当你是在用样本推断总体时才需要关注 sampling 的值。其次是 std_deviation_bounds。它不是简单的最大值最小值而是均值加减两倍标准差也就是一个大致覆盖 95% 数据分布的范围。这个边界在异常检测里非常好用如果某一天的订单金额均值变化不大但上下边界突然扩大了很多说明数据分布变散了大概率出现了极端订单。3.3 用一个真实场景练手比如电商平台想判断大促当天的订单金额是整体上涨还是局部拉升。直接全量跑一个 stats 没什么感觉更好的是配合时间范围过滤来看某一天的分布。GET /order_info/_search { size: 0, query: { range: { order_date: { gte: 2024-11-11T00:00:00, lte: 2024-11-11T23:59:59 } } }, aggs: { amount_ext_stats: { extended_stats: { field: amount } } } }跑完之后如果发现均值比日常涨了 30%但标准差涨了一倍还多那就要警惕“局部拉升”。这时候再配合 terms 按商品类目分桶很快就能定位到底是哪个类目在贡献极端值。4. 百分位percentiles 聚合排查耗时的利器4.1 为什么平均数不靠谱P95 才靠谱先说一个我在实际业务里反复跟人强调过的问题平均值不适合描述耗时。原因很简单任何一个超级慢的请求都会把平均值拉高但绝大多数请求可能都很快。举个极端例子一百个请求里九十九个都是 50ms有一个是 5s平均值算出来是 99.5ms。这个数字看起来还行但实际上绝大多数用户的真实感知是 50ms完全被平均值带偏了。反过来P99 的意思是“99% 的请求都在某个阈值以下”这个阈值更贴近用户的真实体验。所以现在但凡正经一点的监控系统都会把 P95、P99 作为核心指标。4.2 近似算法的原理与 compression 参数percentiles 聚合底层用的是 TDigest 算法。TDigest 会把数据映射到多个质心centroid上做压缩存储所以它的结果天然是估算的不是精确值。和 cardinality 类似它允许你用参数在精度和内存之间做权衡这个参数叫 compression默认值是 100。compression 越大质心数量越多逼近结果越准相应的内存占用也越高。一般来说默认参数在大部分场景下够用。如果你对某几个分位数的精度特别敏感可以微调 compression但我不建议拉得过高否则高基数高并发场景下内存开销会有点难受。另一个实用细节是 percents 参数。默认情况下percentiles 返回 1、5、25、50、75、95、99 这几个分位。但很多时候你只需要其中几个可以在请求里显式指定GET /order_info/_search { size: 0, aggs: { amount_percentiles: { percentiles: { field: amount, percents: [50, 95, 99, 99.9] } } } }返回结果默认是对象形式{ aggregations: { amount_percentiles: { values: { 50.0: 120.5, 95.0: 780.2, 99.0: 1500.0, 99.9: 4900.8 } } } }如果你写程序解析对象不方便可以加一个 keyed: false返回就会变成数组形式。4.3 实操接口响应时延监控与 percentile_rankspercentiles 和 percentile_ranks 是互补关系前者给定百分位求阈值后者给定阈值求百分位。假设我们有一个 access_logs 索引字段 latency 记录每次请求的耗时。想监控线上接口的 P99直接这样查GET /access_logs/_search { size: 0, aggs: { latency_percentiles: { percentiles: { field: latency, percents: [50, 95, 99] } } } }如果返回的 99.0 是 420ms那就说明线上 99% 的请求耗时都控制在 420ms 以内。只要你提前定好目标阈值比如 400ms一眼就能看出是否达标。有时候我们想知道“多少比例的请求超过了 500ms”这时候 percentile_ranks 更合适GET /access_logs/_search { size: 0, aggs: { latency_ranks: { percentile_ranks: { field: latency, values: [100, 500] } } } }返回结果里会看到 100.0 和 500.0 各自对应的百分位。假如 500.0 返回的是 96.2说明 96.2% 的请求耗时在 500ms 以内那超过 500ms 的请求占比就是 3.8%。在实际监控里我最常用的是把 percentiles 和 date_histogram 组合按小时看 P50、P95、P99 的走势曲线。如果 P95 突然抬升但 P50 没变化说明有一小撮慢请求变严重了如果 P50 和 P95 一起抬升那就是整体变慢大概率是下游依赖或者机房网络出了问题。这两类问题的排查方向完全不同分位数一眼就能帮你区分开。5. 字符串统计string_stats 聚合冷门但好用5.1 string_stats 输出什么string_stats 是四个聚合里最冷门的一个它专门处理字符串字段返回以下统计项count参与统计的文档数min_length字段值的最短长度max_length字段值的最长长度avg_length字段值的平均长度entropy字符信息熵通过参数显式开启这个聚合从 7.6 版本开始提供早期版本中处于实验性状态。使用前提是字段必须支持 doc_values一般就是 keyword 类型。text 字段如果要直接用得先开启 fielddata但我强烈不建议这么干正确姿势是使用 keyword 子字段。来看一个日志场景的例子GET /access_logs/_search { size: 0, aggs: { path_stats: { string_stats: { field: request_path.keyword, entropy: true } } } }返回结果{ aggregations: { path_stats: { count: 10000, min_length: 1, max_length: 512, avg_length: 128.6, entropy: 4.32 } } }看到这几项数据你就能快速知道这批请求路径整体长什么样最短的是根路径最长的是拼了一堆参数的完整 URL平均长度一百多字节字符分布熵值中等。5.2 实操日志分类与字段长度分析string_stats 最常见的一个用途是快速定位“长度异常”的字段。举个例子你怀疑日志里的 request_path 被某些攻击脚本拼接得很长正常请求路径一般不超过 200 字节但某段时间 max_length 突然飙到几千。先跑一个 string_stats看到 max_length 异常巨大再用 sort 或者 terms 把最长的几个 URL 揪出来分析问题基本就能锁定。我还用它对 username 字段做过数据质量检查。业务上用户名长度应该集中在 6 到 20 个字符之间结果发现 avg_length 高得离谱一排查原来是写入端把一串序列化的 JSON 错误地塞进了 username 字段。这种脏数据问题不看长度分布根本发现不了。5.3 关于 entropy 的一点理解entropy 返回的是字段值的字符信息熵用来衡量字符集合的混乱程度。数值越大说明字符越多样、越随机数值越小说明字符越集中、越有规律。固定格式的日志路径比如 /api/v1/user/info字符构成非常规律熵会比较低。如果路径里带了随机 token、时间戳这些动态内容熵值就会明显拉高。这个指标在安全分析里有点用比如判断某个 query 参数是不是脚本随机拼出来的。但要注意熵只是一个宏观指标不能当成唯一判断依据需要配合样本内容才能下结论。6. 组合起来用多个指标聚合嵌套在桶聚合里6.1 terms cardinality stats 的组合单个指标聚合能力有限真正发挥威力的是和桶聚合嵌套。最常见的组合是外层 terms 按类目分组内层挂多个指标聚合一次请求拿到一组多维统计。GET /order_info/_search { size: 0, aggs: { by_category: { terms: { field: category.keyword, size: 10 }, aggs: { distinct_users: { cardinality: { field: user_id } }, amount_stats: { stats: { field: amount } }, latency_percentiles: { percentiles: { field: process_time, percents: [95, 99] } } } } } }这个请求返回的是每个商品类目下有多少独立用户、订单金额的 count/min/max/avg/sum以及处理耗时的 P95、P99。三个指标聚合平行嵌在同一个桶下互不干扰这也是指标聚合最标准的组合姿势。返回结构是一个嵌套的 buckets 数组外层每个 bucket 里都会带 distinct_users、amount_stats、latency_percentiles 三个子聚合结果。解析的时候按层级取数就行。6.2 date_histogram 统计每天的 UV 和点击量业务上经常要看趋势曲线date_histogram 和 cardinality 是绝配。比如按天统计 UVGET /order_info/_search { size: 0, aggs: { daily_orders: { date_histogram: { field: order_date, calendar_interval: day }, aggs: { uv: { cardinality: { field: user_id } } } } } }返回里每个日期一个 bucket包含 key_as_string格式化后的日期和 doc_count当天订单数再加上子聚合里的 uv 值。拿这些数据直接画一条 UV 曲线性能比把原始数据拉回内存自己数好太多倍。这里有一个很重要的性能认知如果数据量很大把几百万条文档拉到应用服务器然后自己在内存里做 Hash 去重、排序、算分位数代价极大。Elasticsearch 的聚合框架就是干这个的能下推到集群里的计算就别搬回来。6.3 组合查询失败的几个常见原因组合聚合本身没什么特别的黑魔法但出了问题的时候先把原因归类外层桶聚合的字段类型不对比如对 text 字段做 terms大概率报错。内层指标聚合的字段类型不支持比如对 text 字段做 cardinality 或 percentiles。桶大小设置过大比如 terms 的 size 设成几万返回结构巨大自伤性能。多个聚合嵌套过深层级太多导致查询解析变慢维护起来也麻烦。排查时先拿到一条 min 请求把外桶去掉单独跑指标聚合确认是字段问题还是桶聚合问题再逐步加回去。6.4 常见报错与排查速查表把我在实际环境中踩过、帮别人排查过的坑整理成一张速查表报错信息出现原因解决办法Text fields are not optimised for operations that require per-document field data对 text 字段直接做聚合改用 keyword 子字段或在 mapping 中显式添加 multi-fieldsFielddata is disabled on text fields by default想对 text 字段开启聚合优先加 keyword 子字段不要直接开 fielddata会消耗堆内存cardinality[precision_threshold] must be between 0 and 40000precision_threshold 参数超出合法范围改成 0 到 40000 之间的合法值No mapping found for [xxx] in order to aggregate on字段名写错或该字段没有 mapping用 GET index/_mapping 检查字段是否存在Unknown aggregation type [string_stats]集群版本过低不支持该聚合将集群升级到 7.6 及以上版本unexpected field [entropy]当前版本不支持 entropy 参数或者参数位置写错确认 string_stats 的版本支持范围entropy 需要放在请求体里正确的位置其中 text 字段聚合那个错是最常见的很多新手第一次跑聚合就会撞上。这里多说一句text 类型主要给全文检索用默认不生成 doc_values所以不适合做聚合、排序这类操作。正确的做法是在 mapping 里给字段加一个 keyword 子字段类似这样{ mappings: { properties: { message: { type: text, fields: { keyword: { type: keyword, ignore_above: 256 } } } } } }查询时用 message.keyword 做聚合用 message 做全文搜索两不耽误。7. 写在最后的一点经验这四个指标聚合我用了好几年说点个人体会。cardinality 适合所有“去重后有多少”的问题但因为是近似算法做数据看板之前先跟业务口径对齐stats 和 extended_stats 适合日常数值分布摸底重点看标准差和上下边界不要只盯着平均数percentiles 几乎成了耗时排查的标配监控告警里建议直接用 P99 而不是平均值string_stats 虽然冷门但做日志质量分析和异常发现确实能省不少力气。最后再分享一个小技巧遇到拿不准的聚合先用小索引 sample 一批数据跑一遍确认返回结构符合预期再把同样的 DSL 放到生产索引上。别小看这一步它能帮你避开很多字段类型不匹配、参数范围超限的低级错误尤其是字段多、类型杂的历史索引这种检查更是省不了。
RELATED

相关推荐

MP2642GR-Z与ATmega6450协同实现高可靠电池均衡

MP2642GR-Z与ATmega6450协同实现高可靠电池均衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 5:21:40
Zabbix告警收不到?用Python脚本实现稳定SMTP邮件告警配置指南

Zabbix告警收不到?用Python脚本实现稳定SMTP邮件告警配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 5:21:40
A*寻路算法从原理到实战:启发函数、性能调优与避坑指南

A*寻路算法从原理到实战:启发函数、性能调优与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 5:16:40
MORE NEWS

更多资讯

📰

Flink算子链深度解析:原理、调优与实战避坑指南

1. 算子链到底解决了什么问题第一次接触 Flink 的算子链(Operator Chaining)机制,是在一个实时计算任务里。当时任务逻辑并不复杂,从数据源读取、做一次 map 清洗、再 filter 掉无效记录、最后 keyBy 后做窗口聚合输出。但打开 Fl…

📰

技术参考资源整理指南:从分层筛选到知识库搭建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

揭秘 vm2api 0 提示词注入机制:为什么不用再篡改 System 提示词

【免费下载链接】vm2api vm2api 使用虚拟机完全拟真方案的订阅转api 项目地址: https://gitcode.com/gh_mirrors/vm/vm2api 点击查看 免费下载 vm2api 是一个使用虚拟机完全拟真方案做 AI 订阅转 API 的开源网关,它的核心卖点之一,就是不用向…

📰

两串电池主动均衡:MP2642GR-Z+TM4C1294硬件与固件方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

红队、CTF、企业评估:PentAGI 三种打开方式催生的新玩法

红队、CTF、企业评估:PentAGI 三种打开方式催生的新玩法 【免费下载链接】pentagi Fully autonomous AI Agents system capable of performing complex penetration testing tasks 项目地址: https://gitcode.com/GitHub_Trending/pe/pentagi 一句话下指令&a…

📰

WelcomeCoordinator 架构全景图:核心组件协作与数据流深度解读

【免费下载链接】welcome-coordinator Welcome Coordinator for Android 项目地址: https://gitcode.com/gh_mirrors/we/welcome-coordinator 点击查看 免费下载 WelcomeCoordinator 是一款来自 Redbooth 开源团队的 Android 引导页库(Welcome Wizard /…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬