新增特定聚合导致Elasticsearch查询性能骤降15倍(结果集仅107条)
这种情况我之前在项目里碰到过好几次,明明结果集才107条,加了个brand的聚合就从100ms慢到1.5s,15倍的性能落差确实挺让人崩溃的!
先给你拆解下核心问题:虽然你的查询结果只有107条,但brand字段是全局高基数(200k唯一值),Elasticsearch的terms聚合默认依赖**全局序数(Global Ordinals)**来做统计——简单说就是ES会为高基数keyword字段生成一个“字典映射”,把每个唯一值映射成一个整数,这样聚合时计算更快。但问题在于,这个全局序数的构建/加载成本是和字段的全局唯一值数量挂钩的,而不是和你当前查询的结果集大小挂钩!哪怕你只查107条数据,第一次执行聚合时ES还是得加载整个brand字段的全局序数到内存,这个开销远超过处理107条数据的成本,直接把查询时间拉爆了。
给你几个亲测有效的解决办法,按优先级从高到低来:
给
brand字段开启预构建全局序数
把brand的mapping改成下面这样,让ES在索引刷新时就提前构建好全局序数,而不是等查询时临时构建:"brand": { "type": "keyword", "eager_global_ordinals": true }改完mapping后记得刷新索引,之后再执行原查询,速度应该会直接回到100ms左右的水平。这个是解决高基数字段聚合慢的终极方案,我在多个项目里都用过,效果立竿见影。
在聚合里指定
execution_hint: map
如果暂时不想改mapping,可以先给values_brand聚合加个execution_hint参数,强制ES用哈希表直接统计结果集里的brand值,而不用全局序数:"values_brand": { "terms": { "size": 5, "field": "brand", "execution_hint": "map" } }因为你的结果集只有107条,用哈希表统计的开销极小,完全绕开了全局序数的加载成本,临时救急特别好用。
预热全局序数
如果是冷启动场景(比如索引刚创建、刚重启ES),可以先手动跑一个简单的聚合来预热brand字段的全局序数:GET /your_index_name/_search?size=0 { "aggs": { "warmup_brand": { "terms": { "field": "brand", "size": 1 } } } }跑完这个预热查询后,再执行你的原查询,速度也会快很多。不过这个方法是临时的,因为索引刷新后全局序数可能会重建,需要重新预热。
用profile工具精准定位
如果你还想确认是不是全局序数的问题,可以给原查询加个profile: true参数,看看具体哪个步骤耗时最长:{ "track_total_hits": true, "size": 100, "profile": true, "query": { ... }, "aggs": { ... } }执行后看profile结果里的“aggregations”部分,如果看到和“global ordinals”相关的步骤耗时占了90%以上,那就坐实了是全局序数的问题,直接用上面的1、2方案解决就行。
另外提一句,你用的是ES9.0.3,这个版本对高基数字段的聚合已经有不少优化了,但架不住全局序数的加载成本确实高,尤其是高基数字段。上面的几个方案应该能完美解决你的问题。
内容来源于stack exchange

